From 7a3bf2b759804a08817b3cf0f26a7eabad2f9d7c Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 26 Sep 2023 15:05:17 -0400 Subject: [PATCH 01/79] feat(app): add enable status light section to robotsettings (#13655) * feat(app): add enable status light section to robotsettings --- .../localization/en/device_settings.json | 108 +++++++++--------- .../AdvancedTab/EnableStatusLight.tsx | 52 +++++++++ .../__tests__/EnableStatusLight.test.tsx | 50 ++++++++ .../RobotSettings/AdvancedTab/index.ts | 5 +- .../RobotSettings/RobotSettingsAdvanced.tsx | 14 ++- .../__tests__/RobotSettingsAdvanced.test.tsx | 33 ++++-- 6 files changed, 196 insertions(+), 66 deletions(-) create mode 100644 app/src/organisms/Devices/RobotSettings/AdvancedTab/EnableStatusLight.tsx create mode 100644 app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/EnableStatusLight.test.tsx diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 3a30963c342..c74b84f2dd1 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -1,12 +1,12 @@ { "about_advanced": "About", - "about_calibration_description": "For the robot to move accurately and precisely, you need to calibrate it. Positional calibration happens in three parts: deck calibration, pipette offset calibration and tip length calibration.", "about_calibration_description_ot3": "For the robot to move accurately and precisely, you need to calibrate it. Pipette and gripper calibration is an automated process that uses a calibration probe or pin.After calibration is complete, you can save the calibration data to your computer as a JSON file.", + "about_calibration_description": "For the robot to move accurately and precisely, you need to calibrate it. Positional calibration happens in three parts: deck calibration, pipette offset calibration and tip length calibration.", "about_calibration_title": "About Calibration", "advanced": "Advanced", "alpha_description": "Warning: alpha releases are feature-complete but may contain significant bugs.", - "alternative_security_types": "Alternative security types", "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", + "alternative_security_types": "Alternative security types", "app_change_in": "App Changes in {{version}}", "apply_historic_offsets": "Apply Labware Offsets", "are_you_sure_you_want_to_disconnect": "Are you sure you want to disconnect from {{ssid}}?", @@ -14,54 +14,54 @@ "boot_scripts": "Boot scripts", "browse_file_system": "Browse file system", "bug_fixes": "Bug Fixes", - "calibrate_deck": "Calibrate deck", "calibrate_deck_description": "For pre-2019 robots that do not have crosses etched on the deck.", "calibrate_deck_to_dots": "Calibrate deck to dots", + "calibrate_deck": "Calibrate deck", "calibrate_gripper": "Calibrate gripper", "calibrate_module": "Calibrate module", "calibrate_now": "Calibrate now", "calibrate_pipette": "Calibrate Pipette Offset", - "calibration": "Calibration", "calibration_health_check_description": "Check the accuracy of key calibration points without recalibrating the robot.", "calibration_health_check_title": "Calibration Health Check", + "calibration": "Calibration", "change_network": "Change network", "characters_max": "17 characters max", "check_for_updates": "Check for updates", "checking_for_updates": "Checking for updates", - "choose": "Choose...", "choose_network_type": "Choose network type", "choose_reset_settings": "Choose reset settings", + "choose": "Choose...", "clear_all_data": "Clear all data", - "clear_all_stored_data": "Clear all stored data", "clear_all_stored_data_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.", + "clear_all_stored_data": "Clear all stored data", "clear_calibration_data": "Clear calibration data", "clear_data_and_restart_robot": "Clear data and restart robot", "clear_individual_data": "Clear individual data", "clear_option_authorized_keys": "Clear SSH public keys", - "clear_option_boot_scripts": "Clear custom boot scripts", "clear_option_boot_scripts_description": "Clears scripts that modify the robot's behavior when powered on.", + "clear_option_boot_scripts": "Clear custom boot scripts", "clear_option_deck_calibration": "Clear deck calibration", "clear_option_gripper_calibration": "Clear gripper calibration", "clear_option_gripper_offset_calibrations": "Clear gripper calibration", "clear_option_module_calibration": "Clear module calibration", "clear_option_pipette_calibrations": "Clear pipette calibration", "clear_option_pipette_offset_calibrations": "Clear pipette offset calibrations", - "clear_option_runs_history": "Clear protocol run history", "clear_option_runs_history_subtext": "Clears information about past runs of all protocols.", + "clear_option_runs_history": "Clear protocol run history", "clear_option_tip_length_calibrations": "Clear tip length calibrations", "confirm_device_reset_description": "This will permanently delete all protocol, calibration, and other data. You’ll have to redo initial setup before using the robot again.", "confirm_device_reset_heading": "Are you sure you want to reset your device?", - "connect": "Connect", "connect_the_estop_to_continue": "Connect the E-stop to continue", "connect_to_wifi_network": "Connect to Wi-Fi network", - "connect_via": "Connect via {{type}}", "connect_via_usb_description_1": "1. Connect the USB A-to-B cable to the robot’s USB-B port.", "connect_via_usb_description_2": "2. Connect the cable to an open USB port on your computer.", "connect_via_usb_description_3": "3. Launch the Opentrons App on the computer to continue.", - "connected": "Connected", + "connect_via": "Connect via {{type}}", + "connect": "Connect", "connected_network": "Connected Network", "connected_to_ssid": "Connected to {{ssid}}", "connected_via": "Connected via {{networkInterface}}", + "connected": "Connected", "connecting_to": "Connecting to {{ssid}}...", "connection_description_ethernet": "Connect to your lab's wired network.", "connection_description_usb": "Connect directly to a computer (running the Opentrons App).", @@ -69,58 +69,60 @@ "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", "connection_to_robot_lost": "Connection to robot lost", "deck_calibration_description": "Calibrating the deck is required for new robots or after you relocate your robot. Recalibrating the deck will require you to also recalibrate pipette offsets.", - "deck_calibration_missing": "Deck calibration missing", "deck_calibration_missing_no_pipette": "Deck calibration missing. Attach a pipette to perform deck calibration.", + "deck_calibration_missing": "Deck calibration missing", "deck_calibration_modal_description": "Calibrating pipette offset before deck calibration when both are needed isn’t suggested. Calibrating the deck clears all other calibration data. ", "deck_calibration_modal_pipette_description": "Would you like to continue with pipette offset calibration?", "deck_calibration_modal_title": "Are you sure you want to calibrate?", "deck_calibration_recommended": "Deck calibration recommended", "deck_calibration_title": "Deck Calibration", "dev_tools_description": "Access additional logging and feature flags.", - "device_reset": "Device Reset", "device_reset_description": "Reset labware calibration, boot scripts, and/or robot calibration to factory settings.", "device_reset_slideout_description": "Select individual settings to only clear specific data types.", + "device_reset": "Device Reset", "device_resets_cannot_be_undone": "Resets cannot be undone", "directly_connected_to_this_computer": "Directly connected to this computer.", - "disconnect": "Disconnect", "disconnect_from_ssid": "Disconnect from {{ssid}}", - "disconnect_from_wifi": "Disconnect from Wi-Fi", "disconnect_from_wifi_network_failure": "Your robot was unable to disconnect from Wi-Fi network {{ssid}}.", "disconnect_from_wifi_network_success": "Your robot has successfully disconnected from the Wi-Fi network.", + "disconnect_from_wifi": "Disconnect from Wi-Fi", + "disconnect": "Disconnect", "disconnected_from_wifi": "Disconnected from Wi-Fi", "disconnecting_from_wifi_network": "Disconnecting from Wi-Fi network {{ssid}}", "disengaged": "Disengaged", "display_brightness": "Display Brightness", - "display_led_lights": "Status LEDs", "display_led_lights_description": "Control the strip of color lights on the front of the robot.", + "display_led_lights": "Status LEDs", "display_sleep_settings": "Display Sleep Settings", "do_not_turn_off": "Do not turn off the robot while updating", "done": "Done", - "download": "Download", "download_calibration_data": "Download calibration logs", "download_error": "Download error", "download_logs": "Download logs", + "download": "Download", "downloading_logs": "Downloading logs...", "downloading_software": "Downloading software...", "downloading_update": "Downloading update...", "e_stop_connected": "E-stop successfully connected", "e_stop_not_connected": "Connect the E-stop to an auxiliary port on the back of the robot.", + "enable_status_light_description": "Turn on or off the strip of color LEDs on the front of the robot.", + "enable_status_light": "Enable status light", "engaged": "Engaged", "enter_network_name": "Enter network name", "enter_password": "Enter password", - "estop": "E-stop", "estop_disengaged": "E-stop Disengaged", "estop_engaged": "E-stop Engaged", - "estop_missing": "E-stop missing", "estop_missing_description": "Your E-stop could be damaged or detached. {{robotName}} lost its connection to the E-stop, so it canceled the protocol. Connect a functioning E-stop to continue.", - "estop_pressed": "E-stop pressed", + "estop_missing": "E-stop missing", "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have Flex move the gantry to its home position.", - "ethernet": "Ethernet", + "estop_pressed": "E-stop pressed", + "estop": "E-stop", "ethernet_connection_description": "Connect an Ethernet cable to the back of the robot and a network switch or hub.", + "ethernet": "Ethernet", "exit": "exit", - "factory_reset": "Factory Reset", "factory_reset_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.", "factory_reset_modal_description": "This data cannot be retrieved later.", + "factory_reset": "Factory Reset", "factory_resets_cannot_be_undone": "Factory resets cannot be undone.", "failed_to_connect_to_ssid": "Failed to connect to {{ssid}}", "feature_flags": "Feature Flags", @@ -128,8 +130,8 @@ "finish_setup": "Finish setup", "firmware_version": "Firmware Version", "fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health", - "gantry_homing": "Home Gantry on Restart", "gantry_homing_description": "Homes the gantry along the z-axis.", + "gantry_homing": "Home Gantry on Restart", "go_to_advanced_settings": "Go to Advanced App Settings", "gripper_calibration_description": "Gripper calibration uses a metal pin to determine the gripper's exact position relative to precision-cut squares on deck slots.", "gripper_calibration_title": "Gripper Calibration", @@ -142,30 +144,30 @@ "installing_software": "Installing software...", "installing_update": "Installing update...", "ip_address": "IP Address", - "join_other_network": "Join other network", "join_other_network_error_message": "Must be 2–32 characters long", - "jupyter_notebook": "Jupyter Notebook", + "join_other_network": "Join other network", "jupyter_notebook_description": "Open the Jupyter Notebook running on this robot in the web browser. This is an experimental feature.", "jupyter_notebook_link": "Learn more about using Jupyter notebook", - "last_calibrated": "Last calibrated: {{date}}", + "jupyter_notebook": "Jupyter Notebook", "last_calibrated_label": "Last Calibrated", + "last_calibrated": "Last calibrated: {{date}}", "launch_jupyter_notebook": "Launch Jupyter Notebook", "legacy_settings": "Legacy Settings", "mac_address": "MAC Address", "minutes": "{{minute}} minutes", "missing_calibration": "Missing calibration", "model_and_serial": "Pipette Model and Serial", - "module": "Module", - "module_calibration": "Module Calibration", "module_calibration_description": "Module calibration uses a pipette and attached probe to determine the module's exact position relative to the deck.", + "module_calibration": "Module Calibration", + "module": "Module", "mount": "Mount", "name_love_it": "{{name}}, love it!", "name_rule_description": "Enter up to 17 characters (letters and numbers only)", "name_rule_error_exist": "Oops! Name is already in use. Choose a different name.", "name_rule_error_name_length": "Oops! Robot name must follow the character count and limitations", "name_rule_error_too_short": "Oops! Too short. Robot name must be at least 1 character.", - "name_your_robot": "Name your robot", "name_your_robot_description": "Don’t worry, you can always change this in your settings.", + "name_your_robot": "Name your robot", "need_another_security_type": "Need another security type?", "network_name": "Network Name", "network_settings": "Network Settings", @@ -180,25 +182,25 @@ "no_network_found": "No network found", "no_pipette_attached": "No pipette attached", "none_description": "Not recommended", - "not_calibrated": "Not calibrated yet", "not_calibrated_short": "Not calibrated", - "not_connected": "Not connected", + "not_calibrated": "Not calibrated yet", "not_connected_via_ethernet": "Not connected via Ethernet", "not_connected_via_usb": "Not connected via USB", "not_connected_via_wifi": "Not connected via Wi-Fi", "not_connected_via_wired_usb": "Not connected via wired USB", + "not_connected": "Not connected", "not_now": "Not now", "one_hour": "1 hour", "other_networks": "Other Networks", - "password": "Password", "password_error_message": "Must be at least 8 characters", - "pause_protocol": "Pause protocol when robot door opens", + "password": "Password", "pause_protocol_description": "When enabled, opening the robot door during a run will pause the robot after it has completed its current motion.", + "pause_protocol": "Pause protocol when robot door opens", "pipette_calibrations_description": "Pipette calibration uses a metal probe to determine the pipette's exact position relative to precision-cut squares on deck slots.", "pipette_calibrations_title": "Pipette Calibrations", - "pipette_offset_calibration": "pipette offset calibration", "pipette_offset_calibration_missing": "Pipette Offset calibration missing", "pipette_offset_calibration_recommended": "Pipette Offset calibration recommended", + "pipette_offset_calibration": "pipette offset calibration", "pipette_offset_calibrations_history": "See all Pipette Offset Calibration history", "pipette_offset_calibrations_title": "Pipette Offset Calibrations", "privacy": "Privacy", @@ -213,11 +215,11 @@ "recalibration_recommended": "Recalibration recommended", "reinstall": "reinstall", "remind_me_later": "Remind me later", - "rename_robot": "Rename robot", "rename_robot_input_error": "Oops! Robot name must follow the character count and limitations.", "rename_robot_input_limitation_detail": "Please enter 17 characters max using valid inputs: letters and numbers.", "rename_robot_prefer_usb_connection": "To ensure reliable renaming of your robot, please connect to it via USB.", "rename_robot_title": "Rename Robot", + "rename_robot": "Rename robot", "requires_restarting_the_robot": "Updating the robot’s software requires restarting the robot", "reset_to_factory_settings": "Reset to factory settings?", "resets_cannot_be_undone": "Resets cannot be undone", @@ -231,21 +233,21 @@ "robot_name": "Robot Name", "robot_operating_update_available": "Robot Operating System Update Available", "robot_serial_number": "Robot Serial Number", - "robot_server_version": "Robot Server Version", "robot_server_version_ot3_description": "The Opentrons Flex software includes the robot server and the touchscreen display interface.", - "robot_settings": "Robot Settings", + "robot_server_version": "Robot Server Version", "robot_settings_advanced_unknown": "Unknown", + "robot_settings": "Robot Settings", "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", "robot_successfully_connected": "Robot successfully connected to {{networkName}}.", - "robot_system_version": "Robot System Version", "robot_system_version_available": "Robot System Version {{releaseVersion}} available", - "robot_up_to_date": "Robot is up to date", + "robot_system_version": "Robot System Version", "robot_up_to_date_description": "It looks like your robot is already up to date, but if you're experiencing issues you can re-apply the latest update.", + "robot_up_to_date": "Robot is up to date", "robot_update_available": "Robot Update Available", "robot_update_success": "Robot software successfully updated", "search_again": "Search again", - "searching": "Searching", "searching_for_networks": "Searching for networks...", + "searching": "Searching", "security_type": "Security Type", "select_a_network": "Select a network", "select_a_security_type": "Select a security type", @@ -253,26 +255,26 @@ "select_authentication_method": "Select authentication method for your selected network.", "sending_software": "Sending software...", "serial": "Serial", - "share_logs_with_opentrons": "Share Robot logs with Opentrons", - "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", "share_logs_with_opentrons_description_short": "Share anonymous robot logs with Opentrons.", + "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", "share_logs_with_opentrons_short": "Share Robot logs", - "short_trash_bin": "Short trash bin", + "share_logs_with_opentrons": "Share Robot logs with Opentrons", "short_trash_bin_description": "For pre-2019 robots with trash bins that are 55mm tall (instead of 77mm default)", - "show": "Show", + "short_trash_bin": "Short trash bin", "show_password": "Show Password", + "show": "Show", "sign_into_wifi": "Sign into Wi-Fi", "software_is_up_to_date": "Your software is already up to date!", "software_update_error": "Software update error", "some_robot_controls_are_not_available": "Some robot controls are not available when run is in progress", "ssh_public_keys": "SSH public keys", "subnet_mask": "Subnet Mask", - "successfully_connected": "Successfully connected!", "successfully_connected_to_network": "Successfully connected to {{ssid}}!", + "successfully_connected": "Successfully connected!", "supported_protocol_api_versions": "Supported Protocol API Versions", "switch_to_usb_description": "If your network uses a different authentication method, connect to the Opentrons App and finish Wi-Fi setup there.", - "text_size": "Text Size", "text_size_description": "Text on all screens will adjust to the size you choose below.", + "text_size": "Text Size", "tip_length_calibrations_history": "See all Tip Length Calibration history", "tip_length_calibrations_title": "Tip Length Calibrations", "tiprack": "Tip Rack", @@ -286,18 +288,18 @@ "update_complete": "Update complete!", "update_found": "Update found!", "update_robot_now": "Update robot now", - "update_robot_software": "Update robot software manually with a local file (.zip)", "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", "update_robot_software_link": "Launch Opentrons software update page", - "updating": "Updating", + "update_robot_software": "Update robot software manually with a local file (.zip)", "updating_robot_system": "Updating the robot software requires restarting the robot", + "updating": "Updating", "usage_settings": "Usage Settings", - "usb": "USB", "usb_to_ethernet_description": "Looking for USB-to-Ethernet Adapter info?", - "use_older_aspirate": "Use older aspirate behavior", + "usb": "USB", "use_older_aspirate_description": "Aspirate with the less accurate volumetric calibrations that were used before version 3.7.0. Use this if you need consistency with pre-v3.7.0 results. This only affects GEN1 P10S, P10M, P50M, and P300S pipettes.", - "use_older_protocol_analysis_method": "Use older protocol analysis method", + "use_older_aspirate": "Use older aspirate behavior", "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Opentrons Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", + "use_older_protocol_analysis_method": "Use older protocol analysis method", "validating_software": "Validating software...", "view_details": "View details", "view_latest_release_notes_at": "View latest release notes at {{url}}", @@ -312,13 +314,13 @@ "wired_ip": "Wired IP", "wired_mac_address": "Wired MAC Address", "wired_subnet_mask": "Wired Subnet Mask", - "wired_usb": "Wired USB", "wired_usb_description": "Learn about connecting to a robot via USB", + "wired_usb": "Wired USB", "wireless_ip": "Wireless IP", "wireless_mac_address": "Wireless MAC Address", "wireless_subnet_mask": "Wireless Subnet Mask", - "wpa2_personal": "WPA2 Personal", "wpa2_personal_description": "Most labs use this method", + "wpa2_personal": "WPA2 Personal", "yes_clear_data_and_restart_robot": "Yes, clear data and restart robot", "your_mac_address_is": "Your MAC Address is {{macAddress}}", "your_robot_is_ready_to_go": "Your robot is ready to go." diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/EnableStatusLight.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/EnableStatusLight.tsx new file mode 100644 index 00000000000..e20b1c8f05b --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/EnableStatusLight.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + Box, + DIRECTION_COLUMN, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' +import { StyledText } from '../../../../atoms/text' +import { ToggleButton } from '../../../../atoms/buttons' +import { useLEDLights } from '../../hooks' + +interface EnableStatusLightProps { + robotName: string +} +export function EnableStatusLight({ + robotName, +}: EnableStatusLightProps): JSX.Element { + const { t } = useTranslation('device_settings') + const { lightsEnabled, toggleLights } = useLEDLights(robotName) + + return ( + + + + + {t('enable_status_light')} + + {t('enable_status_light_description')} + + + + + ) +} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/EnableStatusLight.test.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/EnableStatusLight.test.tsx new file mode 100644 index 00000000000..510038e4f23 --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/EnableStatusLight.test.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' + +import { renderWithProviders } from '@opentrons/components' + +import { i18n } from '../../../../../i18n' +import { useLEDLights } from '../../../hooks' +import { EnableStatusLight } from '../EnableStatusLight' + +jest.mock('../../../hooks') + +const mockUseLEDLights = useLEDLights as jest.MockedFunction< + typeof useLEDLights +> + +const ROBOT_NAME = 'otie' +const mockToggleLights = jest.fn() +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('EnableStatusLight', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + robotName: ROBOT_NAME, + } + mockUseLEDLights.mockReturnValue({ + lightsEnabled: false, + toggleLights: mockToggleLights, + }) + }) + + it('should render text and toggle button', () => { + const [{ getByText, getByLabelText }] = render(props) + getByText('Enable status light') + getByText( + 'Turn on or off the strip of color LEDs on the front of the robot.' + ) + expect(getByLabelText('enable_status_light')).toBeInTheDocument() + }) + + it('should call a mock function when clicking toggle button', () => { + const [{ getByLabelText }] = render(props) + getByLabelText('enable_status_light').click() + expect(mockToggleLights).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts index 9e7ada15c5c..1c5cb506bc7 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts @@ -1,6 +1,7 @@ -export * from './GantryHoming' -export * from './DisplayRobotName' export * from './DeviceReset' +export * from './DisplayRobotName' +export * from './EnableStatusLight' +export * from './GantryHoming' export * from './LegacySettings' export * from './OpenJupyterControl' export * from './RobotInformation' diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx index d54e195784b..7a744d99dae 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -14,11 +14,11 @@ import { Divider } from '../../../atoms/structure' import { StyledText } from '../../../atoms/text' import { ToggleButton } from '../../../atoms/buttons' import { useIsOT3, useIsRobotBusy, useRobot } from '../hooks' -import { UsageSettings } from './AdvancedTab/UsageSettings' import { - GantryHoming, - DisplayRobotName, DeviceReset, + DisplayRobotName, + EnableStatusLight, + GantryHoming, LegacySettings, OpenJupyterControl, RobotInformation, @@ -26,6 +26,7 @@ import { ShortTrashBin, Troubleshooting, UpdateRobotSoftware, + UsageSettings, UseOlderAspirateBehavior, UseOlderProtocol, } from './AdvancedTab' @@ -182,6 +183,13 @@ export function RobotSettingsAdvanced({ robotName={robotName} isRobotBusy={isRobotBusy} /> + + {isOT3 ? ( + <> + + + + ) : null} diff --git a/app/src/organisms/Devices/RobotSettings/__tests__/RobotSettingsAdvanced.test.tsx b/app/src/organisms/Devices/RobotSettings/__tests__/RobotSettingsAdvanced.test.tsx index 8048be4f284..a767143b07f 100644 --- a/app/src/organisms/Devices/RobotSettings/__tests__/RobotSettingsAdvanced.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/__tests__/RobotSettingsAdvanced.test.tsx @@ -8,9 +8,10 @@ import { i18n } from '../../../../i18n' import { getShellUpdateState } from '../../../../redux/shell' import { useIsOT3 } from '../../hooks' import { - GantryHoming, - DisplayRobotName, DeviceReset, + DisplayRobotName, + EnableStatusLight, + GantryHoming, LegacySettings, OpenJupyterControl, RobotInformation, @@ -33,9 +34,10 @@ jest.mock('../../../../redux/shell/update', () => ({ getShellUpdateState: jest.fn(), })) jest.mock('../../hooks/useIsOT3') +jest.mock('../AdvancedTab/DeviceReset') jest.mock('../AdvancedTab/DisplayRobotName') +jest.mock('../AdvancedTab/EnableStatusLight') jest.mock('../AdvancedTab/GantryHoming') -jest.mock('../AdvancedTab/DeviceReset') jest.mock('../AdvancedTab/LegacySettings') jest.mock('../AdvancedTab/OpenJupyterControl') jest.mock('../AdvancedTab/RobotInformation') @@ -88,6 +90,9 @@ const mockUseOlderAspirateBehavior = UseOlderAspirateBehavior as jest.MockedFunc const mockUseOlderProtocol = UseOlderProtocol as jest.MockedFunction< typeof UseOlderProtocol > +const mockEnableStatusLight = EnableStatusLight as jest.MockedFunction< + typeof EnableStatusLight +> const mockUseIsOT3 = useIsOT3 as jest.MockedFunction const mockUpdateRobotStatus = jest.fn() @@ -137,6 +142,7 @@ describe('RobotSettings Advanced tab', () => {
Mock UseOlderProtocol Section
) when(mockUseIsOT3).calledWith('otie').mockReturnValue(false) + mockEnableStatusLight.mockReturnValue(
mock EnableStatusLight
) }) afterAll(() => { @@ -164,7 +170,7 @@ describe('RobotSettings Advanced tab', () => { getByText('Mock LegacySettings Section') }) - it('should not render LegacySettings section for OT-3', () => { + it('should not render LegacySettings section for Flex', () => { when(mockUseIsOT3).calledWith('otie').mockReturnValue(true) const [{ queryByText }] = render() expect(queryByText('Mock LegacySettings Section')).toBeNull() @@ -190,7 +196,7 @@ describe('RobotSettings Advanced tab', () => { getByText('Mock ShortTrashBin Section') }) - it('should not render ShortTrashBin section for OT-3', () => { + it('should not render ShortTrashBin section for Flex', () => { when(mockUseIsOT3).calledWith('otie').mockReturnValue(true) const [{ queryByText }] = render() expect(queryByText('Mock ShortTrashBin Section')).toBeNull() @@ -211,7 +217,7 @@ describe('RobotSettings Advanced tab', () => { getByText('Mock UsageSettings Section') }) - it('should not render UsageSettings for OT-3', () => { + it('should not render UsageSettings for Flex', () => { when(mockUseIsOT3).calledWith('otie').mockReturnValue(true) const [{ queryByText }] = render() expect(queryByText('Mock UsageSettings Section')).toBeNull() @@ -222,7 +228,7 @@ describe('RobotSettings Advanced tab', () => { getByText('Mock UseOlderAspirateBehavior Section') }) - it('should not render UseOlderAspirateBehavior section for OT-3', () => { + it('should not render UseOlderAspirateBehavior section for Flex', () => { when(mockUseIsOT3).calledWith('otie').mockReturnValue(true) const [{ queryByText }] = render() expect(queryByText('Mock UseOlderAspirateBehavior Section')).toBeNull() @@ -233,9 +239,20 @@ describe('RobotSettings Advanced tab', () => { getByText('Mock UseOlderProtocol Section') }) - it('should not render UseOlderProtocol section for OT-3', () => { + it('should not render UseOlderProtocol section for Flex', () => { when(mockUseIsOT3).calledWith('otie').mockReturnValue(true) const [{ queryByText }] = render() expect(queryByText('Mock UseOlderProtocol Section')).toBeNull() }) + + it('should not render EnableStatusLight section for OT-2', () => { + const [{ queryByText }] = render() + expect(queryByText('mock EnableStatusLight')).not.toBeInTheDocument() + }) + + it('should render EnableStatusLight section for Flex', () => { + when(mockUseIsOT3).calledWith('otie').mockReturnValue(true) + const [{ getByText }] = render() + getByText('mock EnableStatusLight') + }) }) From 67acd58a904124d421c9bf4f781a9d001ea714ce Mon Sep 17 00:00:00 2001 From: Jamey H Date: Tue, 26 Sep 2023 15:06:34 -0400 Subject: [PATCH 02/79] fix(app): remove pipette settings from overflow menu if no pipette settings found (#13654) Closes RQA-1231 and RQA-1228 --- .../__tests__/ConfigurePipette.test.tsx | 10 +---- app/src/organisms/ConfigurePipette/index.tsx | 37 ++++++------------ .../PipetteCard/PipetteOverflowMenu.tsx | 17 ++++++--- .../PipetteCard/PipetteSettingsSlideout.tsx | 13 ++++++- .../__tests__/PipetteCard.test.tsx | 21 ++++++++++ .../__tests__/PipetteOverflowMenu.test.tsx | 33 +++++++++------- .../PipetteSettingsSlideout.test.tsx | 12 +----- .../organisms/Devices/PipetteCard/index.tsx | 38 +++++++++++++------ .../redux/robot-update/__tests__/epic.test.ts | 2 +- app/src/redux/robot-update/epic.ts | 6 +-- 10 files changed, 109 insertions(+), 80 deletions(-) diff --git a/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx b/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx index 0158956dfef..ff7a7abe3e6 100644 --- a/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx +++ b/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx @@ -4,7 +4,6 @@ import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../i18n' import * as RobotApi from '../../../redux/robot-api' import { ConfigurePipette } from '../../ConfigurePipette' -import { getAttachedPipetteSettingsFieldsById } from '../../../redux/pipettes' import { mockPipetteSettingsFieldsMap } from '../../../redux/pipettes/__fixtures__' import { getConfig } from '../../../redux/config' @@ -13,7 +12,6 @@ import type { State } from '../../../redux/types' jest.mock('../../../redux/robot-api') jest.mock('../../../redux/config') -jest.mock('../../../redux/pipettes') const mockGetConfig = getConfig as jest.MockedFunction const mockUseDispatchApiRequest = RobotApi.useDispatchApiRequest as jest.MockedFunction< @@ -22,9 +20,6 @@ const mockUseDispatchApiRequest = RobotApi.useDispatchApiRequest as jest.MockedF const mockGetRequestById = RobotApi.getRequestById as jest.MockedFunction< typeof RobotApi.getRequestById > -const mockGetAttachedPipetteSettingsFieldsById = getAttachedPipetteSettingsFieldsById as jest.MockedFunction< - typeof getAttachedPipetteSettingsFieldsById -> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -40,7 +35,7 @@ describe('ConfigurePipette', () => { beforeEach(() => { props = { - pipetteId: 'id', + settings: mockPipetteSettingsFieldsMap, robotName: mockRobotName, updateRequest: { status: 'pending' }, updateSettings: jest.fn(), @@ -59,9 +54,6 @@ describe('ConfigurePipette', () => { }, }) mockGetConfig.mockReturnValue({} as any) - when(mockGetAttachedPipetteSettingsFieldsById) - .calledWith({} as State, mockRobotName, 'id') - .mockReturnValue(mockPipetteSettingsFieldsMap) dispatchApiRequest = jest.fn() when(mockUseDispatchApiRequest) .calledWith() diff --git a/app/src/organisms/ConfigurePipette/index.tsx b/app/src/organisms/ConfigurePipette/index.tsx index 43b4227b4d7..1353854ff54 100644 --- a/app/src/organisms/ConfigurePipette/index.tsx +++ b/app/src/organisms/ConfigurePipette/index.tsx @@ -1,41 +1,30 @@ import * as React from 'react' -import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { Box } from '@opentrons/components' + import { SUCCESS, FAILURE, PENDING } from '../../redux/robot-api' import { ConfigForm } from './ConfigForm' import { ConfigErrorBanner } from './ConfigErrorBanner' + import type { - AttachedPipette, PipetteSettingsFieldsUpdate, + PipetteSettingsFieldsMap, } from '../../redux/pipettes/types' -import { getAttachedPipetteSettingsFieldsById } from '../../redux/pipettes' import type { RequestState } from '../../redux/robot-api/types' -import type { State } from '../../redux/types' interface Props { closeModal: () => void - pipetteId: AttachedPipette['id'] updateRequest: RequestState | null updateSettings: (fields: PipetteSettingsFieldsUpdate) => void robotName: string formId: string + settings: PipetteSettingsFieldsMap } export function ConfigurePipette(props: Props): JSX.Element { - const { - closeModal, - pipetteId, - updateRequest, - updateSettings, - robotName, - formId, - } = props + const { closeModal, updateRequest, updateSettings, formId, settings } = props const { t } = useTranslation('device_details') - const settings = useSelector((state: State) => - getAttachedPipetteSettingsFieldsById(state, robotName, pipetteId) - ) const groupLabels = [ t('plunger_positions'), t('tip_pickup_drop'), @@ -58,15 +47,13 @@ export function ConfigurePipette(props: Props): JSX.Element { return ( {updateError && } - {settings != null && pipetteId != null && ( - - )} + ) } diff --git a/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx b/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx index 311bf5fbf79..ee1399ea160 100644 --- a/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx +++ b/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' + import { Flex, POSITION_RELATIVE, @@ -8,18 +9,23 @@ import { SPACING, DIRECTION_COLUMN, } from '@opentrons/components' -import { MenuItem } from '../../../atoms/MenuList/MenuItem' -import { Divider } from '../../../atoms/structure' - import { isOT3Pipette, PipetteModelSpecs, PipetteName, } from '@opentrons/shared-data' -import type { Mount } from '../../../redux/pipettes/types' + +import { MenuItem } from '../../../atoms/MenuList/MenuItem' +import { Divider } from '../../../atoms/structure' + +import type { + Mount, + PipetteSettingsFieldsMap, +} from '../../../redux/pipettes/types' interface PipetteOverflowMenuProps { pipetteSpecs: PipetteModelSpecs | null + pipetteSettings: PipetteSettingsFieldsMap | null mount: Mount handleChangePipette: () => void handleCalibrate: () => void @@ -35,6 +41,7 @@ export const PipetteOverflowMenu = ( const { mount, pipetteSpecs, + pipetteSettings, handleChangePipette, handleCalibrate, handleAboutSlideout, @@ -103,7 +110,7 @@ export const PipetteOverflowMenu = ( {t('about_pipette')} - {!isOT3PipetteAttached ? ( + {!isOT3PipetteAttached && pipetteSettings != null ? ( handleSettingsSlideout()} diff --git a/app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx b/app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx index 87a5ada6498..ba1933dc85d 100644 --- a/app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx +++ b/app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx @@ -20,6 +20,7 @@ import { ConfigurePipette } from '../../ConfigurePipette' import type { AttachedPipette, PipetteSettingsFieldsUpdate, + PipetteSettingsFieldsMap, } from '../../../redux/pipettes/types' import type { Dispatch, State } from '../../../redux/types' @@ -31,12 +32,20 @@ interface PipetteSettingsSlideoutProps { onCloseClick: () => void isExpanded: boolean pipetteId: AttachedPipette['id'] + settings: PipetteSettingsFieldsMap } export const PipetteSettingsSlideout = ( props: PipetteSettingsSlideoutProps ): JSX.Element | null => { - const { pipetteName, robotName, isExpanded, pipetteId, onCloseClick } = props + const { + pipetteName, + robotName, + isExpanded, + pipetteId, + onCloseClick, + settings, + } = props const { t } = useTranslation('device_details') const dispatch = useDispatch() const [dispatchRequest, requestIds] = useDispatchApiRequest() @@ -73,11 +82,11 @@ export const PipetteSettingsSlideout = ( diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx index 9bcdeac0e1d..4ec2b1b36d2 100644 --- a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx +++ b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx @@ -7,6 +7,7 @@ import { useCurrentSubsystemUpdateQuery } from '@opentrons/react-api-client' import { i18n } from '../../../../i18n' import { getHasCalibrationBlock } from '../../../../redux/config' import { useDispatchApiRequest } from '../../../../redux/robot-api' +import { getAttachedPipetteSettingsFieldsById } from '../../../../redux/pipettes' import { AskForCalibrationBlockModal } from '../../../CalibrateTipLength' import { useCalibratePipetteOffset } from '../../../CalibratePipetteOffset/useCalibratePipetteOffset' import { useDeckCalibrationData, useIsOT3 } from '../../hooks' @@ -17,9 +18,11 @@ import { PipetteCard } from '..' import { mockLeftSpecs, mockRightSpecs, + mockPipetteSettingsFieldsMap, } from '../../../../redux/pipettes/__fixtures__' import { mockDeckCalData } from '../../../../redux/calibration/__fixtures__' +import type { State } from '../../../../redux/types' import type { DispatchApiRequestType } from '../../../../redux/robot-api' jest.mock('../PipetteOverflowMenu') @@ -30,6 +33,7 @@ jest.mock('../../hooks') jest.mock('../AboutPipetteSlideout') jest.mock('../../../../redux/robot-api') jest.mock('@opentrons/react-api-client') +jest.mock('../../../../redux/pipettes') const mockPipetteOverflowMenu = PipetteOverflowMenu as jest.MockedFunction< typeof PipetteOverflowMenu @@ -56,6 +60,9 @@ const mockUseIsOT3 = useIsOT3 as jest.MockedFunction const mockUseCurrentSubsystemUpdateQuery = useCurrentSubsystemUpdateQuery as jest.MockedFunction< typeof useCurrentSubsystemUpdateQuery > +const mockGetAttachedPipetteSettingsFieldsById = getAttachedPipetteSettingsFieldsById as jest.MockedFunction< + typeof getAttachedPipetteSettingsFieldsById +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -105,6 +112,9 @@ describe('PipetteCard', () => { mockUseCurrentSubsystemUpdateQuery.mockReturnValue({ data: undefined, } as any) + when(mockGetAttachedPipetteSettingsFieldsById) + .calledWith({} as State, mockRobotName, 'id') + .mockReturnValue(mockPipetteSettingsFieldsMap) }) afterEach(() => { jest.resetAllMocks() @@ -274,4 +284,15 @@ describe('PipetteCard', () => { getByText('Instrument attached') getByText('Firmware update in progress...') }) + it('does not render a pipette settings slideout card if the pipette has no settings', () => { + when(mockGetAttachedPipetteSettingsFieldsById) + .calledWith({} as State, mockRobotName, 'id') + .mockReturnValue(null) + const { queryByTestId } = render(props) + expect( + queryByTestId( + `PipetteSettingsSlideout_${mockRobotName}_${props.pipetteId}` + ) + ).not.toBeInTheDocument() + }) }) diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx index 35eb8705eee..00d6a6d2355 100644 --- a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx @@ -4,7 +4,10 @@ import { resetAllWhenMocks } from 'jest-when' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../../i18n' import { PipetteOverflowMenu } from '../PipetteOverflowMenu' -import { mockLeftProtoPipette } from '../../../../redux/pipettes/__fixtures__' +import { + mockLeftProtoPipette, + mockPipetteSettingsFieldsMap, +} from '../../../../redux/pipettes/__fixtures__' import { isOT3Pipette } from '@opentrons/shared-data' import type { Mount } from '../../../../redux/pipettes/types' @@ -35,6 +38,7 @@ describe('PipetteOverflowMenu', () => { beforeEach(() => { props = { pipetteSpecs: mockLeftProtoPipette.modelSpecs, + pipetteSettings: mockPipetteSettingsFieldsMap, mount: LEFT, handleChangePipette: jest.fn(), handleCalibrate: jest.fn(), @@ -62,13 +66,8 @@ describe('PipetteOverflowMenu', () => { }) it('renders information with no pipette attached', () => { props = { + ...props, pipetteSpecs: null, - mount: LEFT, - handleChangePipette: jest.fn(), - handleCalibrate: jest.fn(), - handleAboutSlideout: jest.fn(), - handleSettingsSlideout: jest.fn(), - isPipetteCalibrated: false, } const { getByRole } = render(props) const btn = getByRole('button', { name: 'Attach pipette' }) @@ -78,14 +77,8 @@ describe('PipetteOverflowMenu', () => { it('renders recalibrate pipette text for OT-3 pipette', () => { mockIsOT3Pipette.mockReturnValue(true) - props = { - pipetteSpecs: mockLeftProtoPipette.modelSpecs, - mount: LEFT, - handleChangePipette: jest.fn(), - handleCalibrate: jest.fn(), - handleAboutSlideout: jest.fn(), - handleSettingsSlideout: jest.fn(), + ...props, isPipetteCalibrated: true, } const { getByRole } = render(props) @@ -130,4 +123,16 @@ describe('PipetteOverflowMenu', () => { fireEvent.click(about) expect(props.handleAboutSlideout).toHaveBeenCalled() }) + + it('does not render the pipette settings button if the pipette has no settings', () => { + mockIsOT3Pipette.mockReturnValue(false) + props = { + ...props, + pipetteSettings: null, + } + const { queryByRole } = render(props) + const settings = queryByRole('button', { name: 'Pipette Settings' }) + + expect(settings).not.toBeInTheDocument() + }) }) diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx index 104d169ecba..546d3774d8b 100644 --- a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx +++ b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx @@ -5,10 +5,7 @@ import { fireEvent } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../../i18n' import * as RobotApi from '../../../../redux/robot-api' -import { - getAttachedPipetteSettingsFieldsById, - updatePipetteSettings, -} from '../../../../redux/pipettes' +import { updatePipetteSettings } from '../../../../redux/pipettes' import { getConfig } from '../../../../redux/config' import { PipetteSettingsSlideout } from '../PipetteSettingsSlideout' @@ -31,9 +28,6 @@ const mockUseDispatchApiRequest = RobotApi.useDispatchApiRequest as jest.MockedF const mockGetRequestById = RobotApi.getRequestById as jest.MockedFunction< typeof RobotApi.getRequestById > -const mockGetAttachedPipetteSettingsFieldsById = getAttachedPipetteSettingsFieldsById as jest.MockedFunction< - typeof getAttachedPipetteSettingsFieldsById -> const mockUpdatePipetteSettings = updatePipetteSettings as jest.MockedFunction< typeof updatePipetteSettings > @@ -56,6 +50,7 @@ describe('PipetteSettingsSlideout', () => { beforeEach(() => { props = { pipetteId: 'id', + settings: mockPipetteSettingsFieldsMap, robotName: mockRobotName, pipetteName: mockLeftSpecs.displayName, isExpanded: true, @@ -71,9 +66,6 @@ describe('PipetteSettingsSlideout', () => { }, }) mockGetConfig.mockReturnValue({} as any) - mockGetAttachedPipetteSettingsFieldsById.mockReturnValue( - mockPipetteSettingsFieldsMap - ) dispatchApiRequest = jest.fn() when(mockUseDispatchApiRequest) .calledWith() diff --git a/app/src/organisms/Devices/PipetteCard/index.tsx b/app/src/organisms/Devices/PipetteCard/index.tsx index 8c1f032922b..d9fe1f693aa 100644 --- a/app/src/organisms/Devices/PipetteCard/index.tsx +++ b/app/src/organisms/Devices/PipetteCard/index.tsx @@ -1,6 +1,8 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' +import { useSelector } from 'react-redux' + import { Box, Flex, @@ -20,7 +22,11 @@ import { SINGLE_MOUNT_PIPETTES, } from '@opentrons/shared-data' import { useCurrentSubsystemUpdateQuery } from '@opentrons/react-api-client' -import { LEFT } from '../../../redux/pipettes' + +import { + LEFT, + getAttachedPipetteSettingsFieldsById, +} from '../../../redux/pipettes' import { OverflowBtn } from '../../../atoms/MenuList/OverflowBtn' import { StyledText } from '../../../atoms/text' import { Banner } from '../../../atoms/Banner' @@ -34,6 +40,8 @@ import { useIsOT3 } from '../hooks' import { PipetteOverflowMenu } from './PipetteOverflowMenu' import { PipetteSettingsSlideout } from './PipetteSettingsSlideout' import { AboutPipetteSlideout } from './AboutPipetteSlideout' + +import type { State } from '../../../redux/types' import type { PipetteModelSpecs, PipetteMount, @@ -47,13 +55,13 @@ import type { interface PipetteCardProps { pipetteModelSpecs: PipetteModelSpecs | null - pipetteId?: AttachedPipette['id'] | null isPipetteCalibrated: boolean mount: Mount robotName: string pipetteIs96Channel: boolean pipetteIsBad: boolean updatePipette: () => void + pipetteId?: AttachedPipette['id'] | null } const BANNER_LINK_CSS = css` text-decoration: underline; @@ -103,6 +111,9 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { refetchInterval: SUBSYSTEM_UPDATE_POLL_MS, } ) + const settings = useSelector((state: State) => + getAttachedPipetteSettingsFieldsById(state, robotName, pipetteId ?? '') + ) const [ selectedPipette, @@ -173,15 +184,19 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { closeModal={() => setChangePipette(false)} /> )} - {showSlideout && pipetteModelSpecs != null && pipetteId != null && ( - setShowSlideout(false)} - isExpanded={true} - pipetteId={pipetteId} - /> - )} + {showSlideout && + pipetteModelSpecs != null && + pipetteId != null && + settings != null && ( + setShowSlideout(false)} + isExpanded={true} + pipetteId={pipetteId} + settings={settings} + /> + )} {showAboutSlideout && pipetteModelSpecs != null && pipetteId != null && ( { handleAboutSlideout={handleAboutSlideout} handleCalibrate={handleCalibrate} isPipetteCalibrated={isPipetteCalibrated} + pipetteSettings={settings} /> {menuOverlay} diff --git a/app/src/redux/robot-update/__tests__/epic.test.ts b/app/src/redux/robot-update/__tests__/epic.test.ts index 016e4ed7465..d6ad7d46652 100644 --- a/app/src/redux/robot-update/__tests__/epic.test.ts +++ b/app/src/redux/robot-update/__tests__/epic.test.ts @@ -207,7 +207,7 @@ describe('robot update epics', () => { expectObservable(output$).toBe('-a', { a: actions.unexpectedRobotUpdateError( - 'This robot must be updated by the system before a custom update can occur.' + 'This robot must be updated by the system before a custom update can occur' ), }) }) diff --git a/app/src/redux/robot-update/epic.ts b/app/src/redux/robot-update/epic.ts index a8e29594c91..a974fa5a1bd 100644 --- a/app/src/redux/robot-update/epic.ts +++ b/app/src/redux/robot-update/epic.ts @@ -97,10 +97,10 @@ const ROBOT_DID_NOT_RECONNECT = 'Robot did not successfully reconnect' const BUT_WE_EXPECTED = 'but we expected' const UNKNOWN = 'unknown' const CHECK_TO_VERIFY_UPDATE = - "Check your robot's settings page to verify whether or not the update was successful." -const UNABLE_TO_FIND_SYSTEM_FILE = 'Unable to find system file for update.' + "Check your robot's settings page to verify whether or not the update was successful" +const UNABLE_TO_FIND_SYSTEM_FILE = 'Unable to find system file for update' const ROBOT_REQUIRES_PREMIGRATION = - 'This robot must be updated by the system before a custom update can occur.' + 'This robot must be updated by the system before a custom update can occur' // listen for the kickoff action and: // if not ready for buildroot, kickoff premigration From f2a12710dc9dbdfe8fb478d8abf7e6e938478abe Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 27 Sep 2023 11:07:08 -0400 Subject: [PATCH 03/79] fix(app): fix running command list highlight issue (#13648) * fix(app): fix running command list highlight issue --- .../RunningProtocolCommandList.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx index 54416babf82..30e421c462f 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx @@ -69,6 +69,11 @@ const COMMAND_ROW_STYLE = css` // backdrop-filter: blur(1.5px); // ` +interface VisibleIndexRange { + lowestVisibleIndex: number + highestVisibleIndex: number +} + interface RunningProtocolCommandListProps { runStatus: RunStatus | null robotSideAnalysis: CompletedProtocolAnalysis | null @@ -100,6 +105,10 @@ export function RunningProtocolCommandList({ if (runStatus === RUN_STATUS_RUNNING) pauseRun() setShowConfirmCancelRunModal(true) } + const [visibleRange, setVisibleRange] = React.useState({ + lowestVisibleIndex: 0, + highestVisibleIndex: 0, + }) const onTogglePlayPause = (): void => { if (runStatus === RUN_STATUS_RUNNING) { @@ -120,6 +129,27 @@ export function RunningProtocolCommandList({ } } + React.useEffect(() => { + // Note (kk:09/25/2023) Need -1 because the element of highestVisibleIndex cannot really readable + // due to limited space + const isCurrentCommandVisible = + currentRunCommandIndex != null && + currentRunCommandIndex >= visibleRange.lowestVisibleIndex && + currentRunCommandIndex <= visibleRange.highestVisibleIndex - 1 + + if ( + ref.current != null && + !isCurrentCommandVisible && + currentRunCommandIndex != null + ) { + ref.current.scrollToIndex(currentRunCommandIndex) + } + }, [ + currentRunCommandIndex, + visibleRange.highestVisibleIndex, + visibleRange.lowestVisibleIndex, + ]) + return ( { + if ( + currentRunCommandIndex != null && + currentRunCommandIndex >= 0 + ) { + setVisibleRange({ + lowestVisibleIndex, + highestVisibleIndex, + }) + } + }} initialIndex={currentRunCommandIndex} margin={0} > From a680b778e3fe562d375e343a06413996ca5b8170 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 27 Sep 2023 12:17:04 -0400 Subject: [PATCH 04/79] fix(app): fix robot status header copy (#13666) Lowercase the robot's current run status if a status header is rendered. This is made cleaner by adding a lowerCase format option to our i18n interpolation. --- app/src/i18n.ts | 1 + app/src/organisms/Devices/RobotStatusHeader.tsx | 7 ++++--- .../organisms/Devices/__tests__/RobotStatusHeader.test.tsx | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/i18n.ts b/app/src/i18n.ts index a605a9a2f39..9e03af972c0 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -25,6 +25,7 @@ i18n.use(initReactI18next).init( escapeValue: false, // not needed for react as it escapes by default format: function (value, format, lng) { if (format === 'upperCase') return value.toUpperCase() + if (format === 'lowerCase') return value.toLowerCase() if (format === 'capitalize') return capitalize(value) if (format === 'sentenceCase') return startCase(value) if (format === 'titleCase') return titleCase(value) diff --git a/app/src/organisms/Devices/RobotStatusHeader.tsx b/app/src/organisms/Devices/RobotStatusHeader.tsx index 608b52e866b..8c4cbdabd9f 100644 --- a/app/src/organisms/Devices/RobotStatusHeader.tsx +++ b/app/src/organisms/Devices/RobotStatusHeader.tsx @@ -46,7 +46,7 @@ const STATUS_REFRESH_MS = 5000 export function RobotStatusHeader(props: RobotStatusHeaderProps): JSX.Element { const { name, local, robotModel, ...styleProps } = props - const { t } = useTranslation([ + const { t, i18n } = useTranslation([ 'devices_landing', 'device_settings', 'run_details', @@ -75,8 +75,9 @@ export function RobotStatusHeader(props: RobotStatusHeaderProps): JSX.Element { paddingRight={SPACING.spacing8} overflowWrap="anywhere" > - {`${truncateString(displayName, 80, 65)}; ${t( - `run_details:status_${currentRunStatus}` + {`${truncateString(displayName, 80, 65)}; ${i18n.format( + t(`run_details:status_${currentRunStatus}`), + 'lowerCase' )}`} { const [{ getByRole, getByText }] = render(props) - getByText('fake protocol name; Running') + getByText('fake protocol name; running') const runLink = getByRole('link', { name: 'Go to Run' }) expect(runLink.getAttribute('href')).toEqual( From 390c04460c1089867c2207549b92248c4aa36874 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 27 Sep 2023 17:41:28 -0400 Subject: [PATCH 05/79] fix(app): fix protocolsetup next step button issue (#13668) * fix(app): fix protocolsetup next step button issue --- .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx index 36f14a0a7ff..286f0a827f2 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -74,26 +74,39 @@ export function ProtocolRunSetup({ const [expandedStepKey, setExpandedStepKey] = React.useState( null ) - const [stepsKeysInOrder, setStepKeysInOrder] = React.useState([ - ROBOT_CALIBRATION_STEP_KEY, - LPC_KEY, - LABWARE_SETUP_KEY, - ]) - React.useEffect(() => { - let nextStepKeysInOrder = stepsKeysInOrder + const stepsKeysInOrder = + protocolData != null + ? [ + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_KEY, + LPC_KEY, + LABWARE_SETUP_KEY, + LIQUID_SETUP_KEY, + ] + : [ROBOT_CALIBRATION_STEP_KEY, LPC_KEY, LABWARE_SETUP_KEY] - if (protocolData != null) { - nextStepKeysInOrder = [ - ROBOT_CALIBRATION_STEP_KEY, - MODULE_SETUP_KEY, - LPC_KEY, - LABWARE_SETUP_KEY, - LIQUID_SETUP_KEY, - ] + const targetStepKeyInOrder = stepsKeysInOrder.filter((stepKey: StepKey) => { + if (protocolData == null) { + return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY } - setStepKeysInOrder(nextStepKeysInOrder) - }, [Boolean(protocolData), protocolData?.commands]) + + if ( + protocolData.modules.length === 0 && + protocolData.liquids.length === 0 + ) { + return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY + } + + if (protocolData.modules.length === 0) { + return stepKey !== MODULE_SETUP_KEY + } + + if (protocolData.liquids.length === 0) { + return stepKey !== LIQUID_SETUP_KEY + } + return true + }) if (robot == null) return null const hasLiquids = protocolData != null && protocolData.liquids?.length > 0 @@ -109,8 +122,8 @@ export function ProtocolRunSetup({ robotName={robotName} runId={runId} nextStep={ - stepsKeysInOrder[ - stepsKeysInOrder.findIndex( + targetStepKeyInOrder[ + targetStepKeyInOrder.findIndex( v => v === ROBOT_CALIBRATION_STEP_KEY ) + 1 ] @@ -154,8 +167,8 @@ export function ProtocolRunSetup({ robotName={robotName} runId={runId} nextStep={ - stepsKeysInOrder.findIndex(v => v === LABWARE_SETUP_KEY) === - stepsKeysInOrder.length - 1 + targetStepKeyInOrder.findIndex(v => v === LABWARE_SETUP_KEY) === + targetStepKeyInOrder.length - 1 ? null : LIQUID_SETUP_KEY } From a4db9ba61586b52971642090f6f1d4dcde26484d Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Thu, 28 Sep 2023 10:00:10 -0400 Subject: [PATCH 06/79] fix(app): filter LPC offsets for duplicates on ODD (#13671) --- app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx index 8bba4649b69..040f75b9833 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx @@ -11,6 +11,7 @@ import { useChainRunCommands } from '../../../resources/runs/hooks' import type { RegisterPositionAction } from '../types' import type { Jog } from '../../../molecules/JogControls' import { WizardRequiredEquipmentList } from '../../../molecules/WizardRequiredEquipmentList' +import { getLatestCurrentOffsets } from '../../Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { getIsOnDevice } from '../../../redux/config' import { NeedHelpLink } from '../../CalibrationPanels' import { useSelector } from 'react-redux' @@ -144,6 +145,7 @@ function ViewOffsets(props: ViewOffsetsProps): JSX.Element { const { existingOffsets, labwareDefinitions } = props const { t, i18n } = useTranslation('labware_position_check') const [showOffsetsTable, setShowOffsetsModal] = React.useState(false) + const latestCurrentOffsets = getLatestCurrentOffsets(existingOffsets) return existingOffsets.length > 0 ? ( <> From 727030b6d00d588dc7a21033c48acbc9a985924d Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 28 Sep 2023 10:03:15 -0400 Subject: [PATCH 07/79] refactor(api): Why does EquipmentBroker, the largest broker, not simply eat the other brokers? (#13647) --- api/.flake8 | 12 ++-- api/src/opentrons/commands/publisher.py | 16 +++--- .../opentrons/{broker.py => legacy_broker.py} | 20 +++++-- .../core/legacy/legacy_protocol_core.py | 8 +-- .../protocol_api/create_protocol_context.py | 8 +-- .../protocol_api/instrument_context.py | 5 +- .../opentrons/protocol_api/module_contexts.py | 4 +- .../protocol_api/protocol_context.py | 6 +- .../protocol_runner/legacy_context_plugin.py | 8 +-- .../protocol_runner/legacy_wrappers.py | 8 +-- .../protocol_runner/protocol_runner.py | 8 +-- api/src/opentrons/simulate.py | 4 +- api/src/opentrons/util/__init__.py | 1 + .../{equipment_broker.py => util/broker.py} | 17 ++---- api/tests/opentrons/broker/__init__.py | 0 .../opentrons/commands/test_publisher.py | 22 +++++--- .../test_protocol_context_implementation.py | 22 ++++---- .../test_heater_shaker_context.py | 24 ++++---- .../protocol_api/test_instrument_context.py | 8 +-- .../test_magnetic_module_context.py | 16 +++--- .../protocol_api/test_module_context.py | 8 +-- .../protocol_api/test_protocol_context.py | 4 +- .../test_temperature_module_context.py | 20 +++---- .../protocol_api/test_thermocycler_context.py | 24 ++++---- .../test_legacy_context_plugin.py | 56 +++++++++---------- .../protocol_runner/test_protocol_runner.py | 19 ++++--- .../test_broker.py => test_legacy_broker.py} | 17 +++--- api/tests/opentrons/util/__init__.py | 1 + api/tests/opentrons/util/test_broker.py | 33 +++++++++++ 29 files changed, 224 insertions(+), 175 deletions(-) rename api/src/opentrons/{broker.py => legacy_broker.py} (66%) rename api/src/opentrons/{equipment_broker.py => util/broker.py} (66%) delete mode 100644 api/tests/opentrons/broker/__init__.py rename api/tests/opentrons/{broker/test_broker.py => test_legacy_broker.py} (83%) create mode 100644 api/tests/opentrons/util/test_broker.py diff --git a/api/.flake8 b/api/.flake8 index 58cf97e19e9..7cf00cb00ec 100644 --- a/api/.flake8 +++ b/api/.flake8 @@ -26,7 +26,6 @@ noqa-require-code = true per-file-ignores = setup.py:ANN,D src/opentrons/__init__.py:ANN,D - src/opentrons/broker.py:ANN,D src/opentrons/execute.py:ANN,D src/opentrons/simulate.py:ANN,D src/opentrons/types.py:ANN,D @@ -40,11 +39,14 @@ per-file-ignores = src/opentrons/resources/*:ANN,D src/opentrons/system/*:ANN,D src/opentrons/tools/*:ANN,D - src/opentrons/util/*:ANN,D + src/opentrons/util/async_helpers.py:ANN,D + src/opentrons/util/logging_config.py:ANN,D + src/opentrons/util/linal.py:ANN,D + src/opentrons/util/entrypoint_util.py:ANN,D + src/opentrons/util/helpers.py:ANN,D tests/opentrons/test_init.py:ANN,D tests/opentrons/test_types.py:ANN,D tests/opentrons/conftest.py:ANN,D - tests/opentrons/broker/*:ANN,D tests/opentrons/calibration_storage/*:ANN,D tests/opentrons/commands/*:ANN,D tests/opentrons/config/*:ANN,D @@ -55,4 +57,6 @@ per-file-ignores = tests/opentrons/protocols/*:ANN,D tests/opentrons/system/*:ANN,D tests/opentrons/tools/*:ANN,D - tests/opentrons/util/*:ANN,D + tests/opentrons/util/test_async_helpers.py:ANN,D + tests/opentrons/util/test_linal.py:ANN,D + tests/opentrons/util/test_entrypoint_util.py:ANN,D diff --git a/api/src/opentrons/commands/publisher.py b/api/src/opentrons/commands/publisher.py index 22b81f0b76f..e8ac35c429d 100644 --- a/api/src/opentrons/commands/publisher.py +++ b/api/src/opentrons/commands/publisher.py @@ -4,7 +4,7 @@ from typing import Any, Callable, Iterator, Optional, TypeVar, cast from uuid import uuid4 -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from .types import ( COMMAND as COMMAND_TOPIC, @@ -17,17 +17,17 @@ class CommandPublisher: """An object with a `Broker` dependency used to publish commands.""" - def __init__(self, broker: Optional[Broker]) -> None: + def __init__(self, broker: Optional[LegacyBroker]) -> None: """Initialize the publisher with a Broker.""" - self._broker = broker or Broker() + self._broker = broker or LegacyBroker() @property - def broker(self) -> Broker: + def broker(self) -> LegacyBroker: """Get the publisher's Broker.""" return self._broker @broker.setter - def broker(self, broker: Broker) -> None: + def broker(self, broker: LegacyBroker) -> None: """Set the publisher's Broker.""" self._broker = broker @@ -60,7 +60,7 @@ def _decorated(*args: Any, **kwargs: Any) -> Any: broker = getattr(args[0], "broker", None) assert isinstance( - broker, Broker + broker, LegacyBroker ), "Only methods of CommandPublisher classes should be decorated." func_sig = _inspect_signature(func) @@ -100,7 +100,7 @@ def _decorated(*args: Any, **kwargs: Any) -> Any: @contextmanager -def publish_context(broker: Broker, command: CommandPayload) -> Iterator[None]: +def publish_context(broker: LegacyBroker, command: CommandPayload) -> Iterator[None]: """Publish messages before and after the `with` block has run. If an `error` is raised in the `with` block, it will be published in the "after" @@ -131,7 +131,7 @@ def _inspect_signature(func: Callable[..., Any]) -> inspect.Signature: def _do_publish( - broker: Broker, + broker: LegacyBroker, message_id: str, command: CommandPayload, when: MessageSequenceId, diff --git a/api/src/opentrons/broker.py b/api/src/opentrons/legacy_broker.py similarity index 66% rename from api/src/opentrons/broker.py rename to api/src/opentrons/legacy_broker.py index 494a3daf463..838a75b7759 100644 --- a/api/src/opentrons/broker.py +++ b/api/src/opentrons/legacy_broker.py @@ -1,3 +1,5 @@ +# noqa: D100 + from __future__ import annotations import logging from typing import Callable, Dict, List @@ -9,7 +11,15 @@ MODULE_LOG = logging.getLogger(__name__) -class Broker: +class LegacyBroker: + """A pub/sub message broker. + + Deprecated: + Use the newer, more generic `opentrons.utils.Broker` class instead. + This class is coupled to old types from `opentrons.commands`. + https://opentrons.atlassian.net/browse/RSS-270 + """ + def __init__(self) -> None: self.subscriptions: Dict[ Literal["command"], @@ -17,7 +27,7 @@ def __init__(self) -> None: ] = {} self.logger = MODULE_LOG - def subscribe( + def subscribe( # noqa: D102 self, topic: Literal["command"], handler: Callable[[types.CommandMessage], None], @@ -34,8 +44,10 @@ def unsubscribe() -> None: return unsubscribe - def publish(self, topic: Literal["command"], message: types.CommandMessage) -> None: + def publish( # noqa: D102 + self, topic: Literal["command"], message: types.CommandMessage + ) -> None: [handler(message) for handler in self.subscriptions.get(topic, [])] - def set_logger(self, logger: logging.Logger) -> None: + def set_logger(self, logger: logging.Logger) -> None: # noqa: D102 self.logger = logger diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index 2c99d22bc85..c542fe3cd10 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -7,7 +7,7 @@ from opentrons_shared_data.robot.dev_types import RobotType from opentrons.types import DeckSlotName, Location, Mount, Point -from opentrons.equipment_broker import EquipmentBroker +from opentrons.util.broker import Broker from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules import AbstractModule, ModuleModel, ModuleType from opentrons.hardware_control.types import DoorState, PauseType @@ -44,7 +44,7 @@ def __init__( api_version: APIVersion, labware_offset_provider: AbstractLabwareOffsetProvider, deck_layout: Deck, - equipment_broker: Optional[EquipmentBroker[LoadInfo]] = None, + equipment_broker: Optional[Broker[LoadInfo]] = None, bundled_labware: Optional[Dict[str, LabwareDefinition]] = None, extra_labware: Optional[Dict[str, LabwareDefinition]] = None, ) -> None: @@ -73,7 +73,7 @@ def __init__( self._api_version = api_version self._labware_offset_provider = labware_offset_provider self._deck_layout = deck_layout - self._equipment_broker = equipment_broker or EquipmentBroker() + self._equipment_broker = equipment_broker or Broker() self._instruments: Dict[Mount, Optional[LegacyInstrumentCore]] = { mount: None for mount in Mount.ot2_mounts() # Legacy core works only on OT2 @@ -97,7 +97,7 @@ def robot_type(self) -> RobotType: return "OT-2 Standard" @property - def equipment_broker(self) -> EquipmentBroker[LoadInfo]: + def equipment_broker(self) -> Broker[LoadInfo]: """A message broker to to publish equipment load events. Subscribers to this broker will be notified with information about every diff --git a/api/src/opentrons/protocol_api/create_protocol_context.py b/api/src/opentrons/protocol_api/create_protocol_context.py index f2d8e492ecb..5a64e70cf99 100644 --- a/api/src/opentrons/protocol_api/create_protocol_context.py +++ b/api/src/opentrons/protocol_api/create_protocol_context.py @@ -4,14 +4,14 @@ from opentrons_shared_data.labware.dev_types import LabwareDefinition -from opentrons.broker import Broker -from opentrons.equipment_broker import EquipmentBroker from opentrons.config import feature_flags from opentrons.hardware_control import ( HardwareControlAPI, ThreadManager, SynchronousAdapter, ) +from opentrons.legacy_broker import LegacyBroker +from opentrons.util.broker import Broker from opentrons.protocol_engine import ProtocolEngine from opentrons.protocol_engine.clients import SyncClient, ChildThreadTransport from opentrons.protocols.api_support.types import APIVersion @@ -46,8 +46,8 @@ def create_protocol_context( deck_type: str, protocol_engine: Optional[ProtocolEngine] = None, protocol_engine_loop: Optional[asyncio.AbstractEventLoop] = None, - broker: Optional[Broker] = None, - equipment_broker: Optional[EquipmentBroker[Any]] = None, + broker: Optional[LegacyBroker] = None, + equipment_broker: Optional[Broker[Any]] = None, use_simulating_core: bool = False, extra_labware: Optional[Dict[str, LabwareDefinition]] = None, bundled_labware: Optional[Dict[str, LabwareDefinition]] = None, diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 05d40155700..08f9c29b9b0 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -7,7 +7,7 @@ CommandPreconditionViolated, CommandParameterLimitViolated, ) -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict from opentrons import types, hardware_control as hc from opentrons.commands import commands as cmds @@ -75,13 +75,12 @@ def __init__( self, core: InstrumentCore, protocol_core: ProtocolCore, - broker: Broker, + broker: LegacyBroker, api_version: APIVersion, tip_racks: List[labware.Labware], trash: labware.Labware, requested_as: str, ) -> None: - super().__init__(broker) self._api_version = api_version self._core = core diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index d4984c66af0..8605069ccc7 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -6,7 +6,7 @@ from opentrons_shared_data.labware.dev_types import LabwareDefinition from opentrons_shared_data.module.dev_types import ModuleModel, ModuleType -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules import ThermocyclerStep from opentrons.commands import module_commands as cmds from opentrons.commands.publisher import CommandPublisher, publish @@ -55,7 +55,7 @@ def __init__( protocol_core: ProtocolCore, core_map: LoadedCoreMap, api_version: APIVersion, - broker: Broker, + broker: LegacyBroker, ) -> None: super().__init__(broker=broker) self._core = core diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index c500722006f..86f121f6183 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -17,7 +17,7 @@ from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.types import Mount, Location, DeckLocation, DeckSlotName -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules.types import MagneticBlockModel from opentrons.commands import protocol_commands as cmds, types as cmd_types @@ -101,7 +101,7 @@ def __init__( self, api_version: APIVersion, core: ProtocolCore, - broker: Optional[Broker] = None, + broker: Optional[LegacyBroker] = None, core_map: Optional[LoadedCoreMap] = None, deck: Optional[Deck] = None, bundled_data: Optional[Dict[str, bytes]] = None, @@ -1002,7 +1002,7 @@ def _create_module_context( protocol_core: ProtocolCore, core_map: LoadedCoreMap, api_version: APIVersion, - broker: Broker, + broker: LegacyBroker, ) -> ModuleTypes: module_cls: Optional[Type[ModuleTypes]] = None if isinstance(module_core, AbstractTemperatureModuleCore): diff --git a/api/src/opentrons/protocol_runner/legacy_context_plugin.py b/api/src/opentrons/protocol_runner/legacy_context_plugin.py index 8d1e9a9b1c4..24a2cda49f6 100644 --- a/api/src/opentrons/protocol_runner/legacy_context_plugin.py +++ b/api/src/opentrons/protocol_runner/legacy_context_plugin.py @@ -5,10 +5,10 @@ from contextlib import ExitStack from typing import Optional -from opentrons.broker import Broker -from opentrons.equipment_broker import EquipmentBroker from opentrons.commands.types import CommandMessage as LegacyCommand +from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_engine import AbstractPlugin, actions as pe_actions +from opentrons.util.broker import Broker from .legacy_wrappers import LegacyLoadInfo from .legacy_command_mapper import LegacyCommandMapper @@ -36,8 +36,8 @@ class LegacyContextPlugin(AbstractPlugin): def __init__( self, - broker: Broker, - equipment_broker: EquipmentBroker[LegacyLoadInfo], + broker: LegacyBroker, + equipment_broker: Broker[LegacyLoadInfo], legacy_command_mapper: Optional[LegacyCommandMapper] = None, ) -> None: """Initialize the plugin with its dependencies.""" diff --git a/api/src/opentrons/protocol_runner/legacy_wrappers.py b/api/src/opentrons/protocol_runner/legacy_wrappers.py index 297bdbf3869..6a816f5e9a1 100644 --- a/api/src/opentrons/protocol_runner/legacy_wrappers.py +++ b/api/src/opentrons/protocol_runner/legacy_wrappers.py @@ -9,8 +9,6 @@ ) from opentrons_shared_data.labware.labware_definition import LabwareDefinition -from opentrons.broker import Broker -from opentrons.equipment_broker import EquipmentBroker from opentrons.calibration_storage.helpers import uri_from_details from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.modules.types import ( @@ -20,8 +18,10 @@ ThermocyclerModuleModel as LegacyThermocyclerModuleModel, HeaterShakerModuleModel as LegacyHeaterShakerModuleModel, ) +from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_engine import ProtocolEngine from opentrons.protocol_reader import ProtocolSource, ProtocolFileRole +from opentrons.util.broker import Broker from opentrons.protocol_api import ( ProtocolContext as LegacyProtocolContext, @@ -124,8 +124,8 @@ def __init__( def create( self, protocol: LegacyProtocol, - broker: Optional[Broker], - equipment_broker: Optional[EquipmentBroker[LegacyLoadInfo]], + broker: Optional[LegacyBroker], + equipment_broker: Optional[Broker[LegacyLoadInfo]], ) -> LegacyProtocolContext: """Create a Protocol API v2 context.""" extra_labware = ( diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index f5b317bf1ee..1b49a159087 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -6,10 +6,9 @@ import anyio -from opentrons.broker import Broker -from opentrons.equipment_broker import EquipmentBroker from opentrons.hardware_control import HardwareControlAPI from opentrons import protocol_reader +from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_reader import ( ProtocolSource, JsonProtocolConfig, @@ -22,6 +21,7 @@ commands as pe_commands, ) from opentrons.protocols.parse import PythonParseMode +from opentrons.util.broker import Broker from .task_queue import TaskQueue from .json_file_reader import JsonFileReader @@ -140,8 +140,8 @@ async def load( equipment_broker = None if protocol.api_level < LEGACY_PYTHON_API_VERSION_CUTOFF: - broker = Broker() - equipment_broker = EquipmentBroker[LegacyLoadInfo]() + broker = LegacyBroker() + equipment_broker = Broker[LegacyLoadInfo]() self._protocol_engine.add_plugin( LegacyContextPlugin(broker=broker, equipment_broker=equipment_broker) diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index 46f5ef37f3a..0ce49687cfe 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -35,7 +35,7 @@ from opentrons.protocol_api import MAX_SUPPORTED_VERSION from opentrons.protocols.duration import DurationEstimator from opentrons.protocols.execution import execute -import opentrons.broker +from opentrons.legacy_broker import LegacyBroker from opentrons.config import IS_ROBOT, JUPYTER_NOTEBOOK_LABWARE_DIR from opentrons import protocol_api from opentrons.commands import types as command_types @@ -101,7 +101,7 @@ class CommandScraper: """ def __init__( - self, logger: logging.Logger, level: str, broker: opentrons.broker.Broker + self, logger: logging.Logger, level: str, broker: LegacyBroker ) -> None: """Build the scraper. diff --git a/api/src/opentrons/util/__init__.py b/api/src/opentrons/util/__init__.py index e69de29bb2d..7a4ce85a7f6 100644 --- a/api/src/opentrons/util/__init__.py +++ b/api/src/opentrons/util/__init__.py @@ -0,0 +1 @@ +"""Things that are independent from Opentrons business logic, like data structures.""" diff --git a/api/src/opentrons/equipment_broker.py b/api/src/opentrons/util/broker.py similarity index 66% rename from api/src/opentrons/equipment_broker.py rename to api/src/opentrons/util/broker.py index e25f3bbf6ef..5459735b1b8 100644 --- a/api/src/opentrons/equipment_broker.py +++ b/api/src/opentrons/util/broker.py @@ -1,4 +1,4 @@ -"""A simple pub/sub message broker for monitoring equipment loads.""" +"""A simple pub/sub message broker.""" from typing import Callable, Generic, Set, TypeVar @@ -7,22 +7,13 @@ _MessageT = TypeVar("_MessageT") -class EquipmentBroker(Generic[_MessageT]): +class Broker(Generic[_MessageT]): """A simple pub/sub message broker. - This is currently meant for monitoring equipment loads - (pipette, labware, and module loads) - on an APIv2 `ProtocolContext`. - - This duplicates much of `opentrons.broker.Broker`, - which covers most other APIv2 events, like aspirates and moves, - but doesn't cover equipment loads. - To cover equipment loads, we felt more comfortable - duplicating `opentrons.broker.Broker`'s responsibilities here - than attempting to extend it without breaking anything. + Subscribers can listen to events. Publishers can push events to all subscribers. """ - def __init__(self) -> None: # noqa: D107 + def __init__(self) -> None: self._callbacks: Set[Callable[[_MessageT], None]] = set() def subscribe(self, callback: Callable[[_MessageT], None]) -> Callable[[], None]: diff --git a/api/tests/opentrons/broker/__init__.py b/api/tests/opentrons/broker/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api/tests/opentrons/commands/test_publisher.py b/api/tests/opentrons/commands/test_publisher.py index 2a008a404ac..f38142984bf 100644 --- a/api/tests/opentrons/commands/test_publisher.py +++ b/api/tests/opentrons/commands/test_publisher.py @@ -4,18 +4,18 @@ import pytest from decoy import Decoy, matchers from typing import Any, Dict, cast -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.commands.types import Command as CommandDict, CommandMessage from opentrons.commands.publisher import CommandPublisher, publish, publish_context @pytest.fixture -def broker(decoy: Decoy) -> Broker: +def broker(decoy: Decoy) -> LegacyBroker: """Return a mocked out Broker.""" - return decoy.mock(cls=Broker) + return decoy.mock(cls=LegacyBroker) -def test_publish_decorator(decoy: Decoy, broker: Broker) -> None: +def test_publish_decorator(decoy: Decoy, broker: LegacyBroker) -> None: """It should publish "before" and "after" messages for decorated methods.""" _act = decoy.mock() @@ -69,7 +69,9 @@ def act(self, foo: str, bar: int) -> None: assert before_message_id.value == after_message_id.value -def test_publish_decorator_with_arg_defaults(decoy: Decoy, broker: Broker) -> None: +def test_publish_decorator_with_arg_defaults( + decoy: Decoy, broker: LegacyBroker +) -> None: """It should pass method argument defaults to the command creator.""" _act = decoy.mock() @@ -118,7 +120,7 @@ def act(self, foo: str, bar: int = 42) -> None: ) -def test_publish_decorator_with_error(decoy: Decoy, broker: Broker) -> None: +def test_publish_decorator_with_error(decoy: Decoy, broker: LegacyBroker) -> None: """It should capture an exception and place it in the "after" message.""" def _get_command_payload(foo: str, bar: int) -> Dict[str, Any]: @@ -169,7 +171,9 @@ def act(self, foo: str, bar: int) -> None: assert before_message_id.value == after_message_id.value -def test_publish_decorator_remaps_instrument(decoy: Decoy, broker: Broker) -> None: +def test_publish_decorator_remaps_instrument( + decoy: Decoy, broker: LegacyBroker +) -> None: """It should pass "self" to command creator arguments named "instrument".""" _act = decoy.mock() @@ -221,7 +225,7 @@ def act(self, foo: str) -> None: ) -def test_publish_context(decoy: Decoy, broker: Broker) -> None: +def test_publish_context(decoy: Decoy, broker: LegacyBroker) -> None: _act = decoy.mock() command = cast( @@ -271,7 +275,7 @@ def _published_func(foo: str, bar: int) -> None: assert before_message_id.value == after_message_id.value -def test_publish_context_with_error(decoy: Decoy, broker: Broker) -> None: +def test_publish_context_with_error(decoy: Decoy, broker: LegacyBroker) -> None: command = cast( CommandDict, {"name": "some_command", "payload": {"foo": "hello", "bar": 42}}, diff --git a/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py b/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py index 883deffed73..6961658b712 100644 --- a/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py +++ b/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py @@ -10,17 +10,17 @@ from opentrons_shared_data.module.dev_types import ModuleDefinitionV3 from opentrons.types import DeckSlotName, Location, Mount, Point -from opentrons.protocol_api import OFF_DECK -from opentrons.equipment_broker import EquipmentBroker +from opentrons.util.broker import Broker + from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict - from opentrons.hardware_control.modules import AbstractModule from opentrons.hardware_control.modules.types import ModuleType, TemperatureModuleModel + from opentrons.protocols import labware as mock_labware from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocol_api.core.legacy.module_geometry import ModuleGeometry -from opentrons.protocol_api import MAX_SUPPORTED_VERSION +from opentrons.protocol_api import MAX_SUPPORTED_VERSION, OFF_DECK from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.legacy.deck import Deck @@ -102,16 +102,16 @@ def mock_labware_offset_provider(decoy: Decoy) -> AbstractLabwareOffsetProvider: @pytest.fixture -def mock_equipment_broker(decoy: Decoy) -> EquipmentBroker[LoadInfo]: +def mock_equipment_broker(decoy: Decoy) -> Broker[LoadInfo]: """Get a mock equipment broker.""" - return decoy.mock(cls=EquipmentBroker) + return decoy.mock(cls=Broker) @pytest.fixture def subject( mock_sync_hardware_api: SyncHardwareAPI, mock_labware_offset_provider: AbstractLabwareOffsetProvider, - mock_equipment_broker: EquipmentBroker[LoadInfo], + mock_equipment_broker: Broker[LoadInfo], mock_deck: Deck, ) -> LegacyProtocolCore: """Get a legacy protocol implementation core with mocked out dependencies.""" @@ -127,7 +127,7 @@ def subject( def test_load_instrument( decoy: Decoy, mock_sync_hardware_api: SyncHardwareAPI, - mock_equipment_broker: EquipmentBroker[LoadInfo], + mock_equipment_broker: Broker[LoadInfo], subject: LegacyProtocolCore, ) -> None: """It should load an instrument core.""" @@ -183,7 +183,7 @@ def test_load_labware( decoy: Decoy, mock_deck: Deck, mock_labware_offset_provider: AbstractLabwareOffsetProvider, - mock_equipment_broker: EquipmentBroker[LoadInfo], + mock_equipment_broker: Broker[LoadInfo], subject: LegacyProtocolCore, ) -> None: """It should load a labware core.""" @@ -278,7 +278,7 @@ def test_load_adapter_raises( def test_load_labware_on_module( decoy: Decoy, mock_labware_offset_provider: AbstractLabwareOffsetProvider, - mock_equipment_broker: EquipmentBroker[LoadInfo], + mock_equipment_broker: Broker[LoadInfo], subject: LegacyProtocolCore, ) -> None: """It should load a labware core.""" @@ -377,7 +377,7 @@ def test_load_module( decoy: Decoy, mock_deck: Deck, mock_sync_hardware_api: SyncHardwareAPI, - mock_equipment_broker: EquipmentBroker[LoadInfo], + mock_equipment_broker: Broker[LoadInfo], subject: LegacyProtocolCore, ) -> None: """It should load a module core. diff --git a/api/tests/opentrons/protocol_api/test_heater_shaker_context.py b/api/tests/opentrons/protocol_api/test_heater_shaker_context.py index c4d99d60789..7ccf84777a7 100644 --- a/api/tests/opentrons/protocol_api/test_heater_shaker_context.py +++ b/api/tests/opentrons/protocol_api/test_heater_shaker_context.py @@ -2,7 +2,7 @@ import pytest from decoy import Decoy, matchers -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.drivers.types import HeaterShakerLabwareLatchStatus from opentrons.hardware_control.modules import TemperatureStatus, SpeedStatus from opentrons.protocols.api_support.types import APIVersion @@ -30,9 +30,9 @@ def mock_core_map(decoy: Decoy) -> LoadedCoreMap: @pytest.fixture -def mock_broker(decoy: Decoy) -> Broker: +def mock_broker(decoy: Decoy) -> LegacyBroker: """Get a mock command message broker.""" - return decoy.mock(cls=Broker) + return decoy.mock(cls=LegacyBroker) @pytest.fixture @@ -47,7 +47,7 @@ def subject( mock_core: HeaterShakerCore, mock_protocol_core: ProtocolCore, mock_core_map: LoadedCoreMap, - mock_broker: Broker, + mock_broker: LegacyBroker, ) -> HeaterShakerContext: """Get a temperature module context with its dependencies mocked out.""" return HeaterShakerContext( @@ -129,7 +129,7 @@ def test_get_labware_latch_status( def test_set_target_temperature( decoy: Decoy, mock_core: HeaterShakerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: HeaterShakerContext, ) -> None: """It should set the temperature via the core.""" @@ -156,7 +156,7 @@ def test_set_target_temperature( def test_wait_for_temperature( decoy: Decoy, mock_core: HeaterShakerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: HeaterShakerContext, ) -> None: """It should wait for temperature via the core.""" @@ -183,7 +183,7 @@ def test_wait_for_temperature( def test_set_and_wait_for_temperature( decoy: Decoy, mock_core: HeaterShakerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: HeaterShakerContext, ) -> None: """It should set and wait for the temperature via the core.""" @@ -226,7 +226,7 @@ def test_set_and_wait_for_temperature( def test_set_and_wait_for_shake_speed( decoy: Decoy, mock_core: HeaterShakerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: HeaterShakerContext, ) -> None: """It should set and wait for shake speed via the core.""" @@ -253,7 +253,7 @@ def test_set_and_wait_for_shake_speed( def test_open_labware_latch( decoy: Decoy, mock_core: HeaterShakerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: HeaterShakerContext, ) -> None: """It should open the labware latch via the core.""" @@ -280,7 +280,7 @@ def test_open_labware_latch( def test_close_labware_latch( decoy: Decoy, mock_core: HeaterShakerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: HeaterShakerContext, ) -> None: """It should close the labware latch via the core.""" @@ -307,7 +307,7 @@ def test_close_labware_latch( def test_deactivate_shaker( decoy: Decoy, mock_core: HeaterShakerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: HeaterShakerContext, ) -> None: """It should stop shaking via the core.""" @@ -334,7 +334,7 @@ def test_deactivate_shaker( def test_deactivate_heater( decoy: Decoy, mock_core: HeaterShakerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: HeaterShakerContext, ) -> None: """It should stop heating via the core.""" diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 3e201fad41e..54ecab4cefb 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -5,7 +5,7 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import] from decoy import Decoy -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.protocols.api_support import instrument as mock_instrument_support from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import ( @@ -72,9 +72,9 @@ def mock_protocol_core(decoy: Decoy) -> ProtocolCore: @pytest.fixture -def mock_broker(decoy: Decoy) -> Broker: +def mock_broker(decoy: Decoy) -> LegacyBroker: """Get a mock command message broker.""" - return decoy.mock(cls=Broker) + return decoy.mock(cls=LegacyBroker) @pytest.fixture @@ -93,7 +93,7 @@ def api_version() -> APIVersion: def subject( mock_instrument_core: InstrumentCore, mock_protocol_core: ProtocolCore, - mock_broker: Broker, + mock_broker: LegacyBroker, mock_trash: Labware, api_version: APIVersion, ) -> InstrumentContext: diff --git a/api/tests/opentrons/protocol_api/test_magnetic_module_context.py b/api/tests/opentrons/protocol_api/test_magnetic_module_context.py index a37a68f5d70..6435d5d8787 100644 --- a/api/tests/opentrons/protocol_api/test_magnetic_module_context.py +++ b/api/tests/opentrons/protocol_api/test_magnetic_module_context.py @@ -2,7 +2,7 @@ import pytest from decoy import Decoy, matchers -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules import MagneticStatus from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError @@ -30,9 +30,9 @@ def mock_core_map(decoy: Decoy) -> LoadedCoreMap: @pytest.fixture -def mock_broker(decoy: Decoy) -> Broker: +def mock_broker(decoy: Decoy) -> LegacyBroker: """Get a mock command message broker.""" - return decoy.mock(cls=Broker) + return decoy.mock(cls=LegacyBroker) @pytest.fixture @@ -47,7 +47,7 @@ def subject( mock_core: MagneticModuleCore, mock_protocol_core: ProtocolCore, mock_core_map: LoadedCoreMap, - mock_broker: Broker, + mock_broker: LegacyBroker, ) -> MagneticModuleContext: """Get a magnetic module context with its dependencies mocked out.""" return MagneticModuleContext( @@ -62,7 +62,7 @@ def subject( def test_disengage( decoy: Decoy, mock_core: MagneticModuleCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: MagneticModuleContext, ) -> None: """It should disengage magnets via the core.""" @@ -92,7 +92,7 @@ def test_get_status( @pytest.mark.parametrize("api_version", [APIVersion(2, 13)]) def test_engage_height_from_home_succeeds_on_low_version( decoy: Decoy, - mock_broker: Broker, + mock_broker: LegacyBroker, mock_core: MagneticModuleCore, subject: MagneticModuleContext, ) -> None: @@ -136,7 +136,7 @@ def test_calibrate_raises_on_high_version( def test_engage_height_from_base( decoy: Decoy, - mock_broker: Broker, + mock_broker: LegacyBroker, mock_core: MagneticModuleCore, subject: MagneticModuleContext, ) -> None: @@ -156,7 +156,7 @@ def test_engage_height_from_base( @pytest.mark.parametrize("api_version", [APIVersion(2, 3)]) def test_engage_offset_from_default( decoy: Decoy, - mock_broker: Broker, + mock_broker: LegacyBroker, mock_core: MagneticModuleCore, subject: MagneticModuleContext, ) -> None: diff --git a/api/tests/opentrons/protocol_api/test_module_context.py b/api/tests/opentrons/protocol_api/test_module_context.py index 6847f2b06e2..6ce8928abc4 100644 --- a/api/tests/opentrons/protocol_api/test_module_context.py +++ b/api/tests/opentrons/protocol_api/test_module_context.py @@ -7,7 +7,7 @@ from opentrons_shared_data.labware.dev_types import LabwareDefinition as LabwareDefDict from opentrons.hardware_control.modules.types import ModuleType, HeaterShakerModuleModel -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.protocols.api_support.types import APIVersion from opentrons.protocol_api import MAX_SUPPORTED_VERSION, ModuleContext, Labware from opentrons.protocol_api.core.common import LabwareCore, ModuleCore, ProtocolCore @@ -34,9 +34,9 @@ def mock_protocol_core(decoy: Decoy) -> ProtocolCore: @pytest.fixture -def mock_broker(decoy: Decoy) -> Broker: +def mock_broker(decoy: Decoy) -> LegacyBroker: """Get a mock command message broker.""" - return decoy.mock(cls=Broker) + return decoy.mock(cls=LegacyBroker) @pytest.fixture @@ -50,7 +50,7 @@ def subject( mock_core: ModuleCore, mock_core_map: LoadedCoreMap, mock_protocol_core: ProtocolCore, - mock_broker: Broker, + mock_broker: LegacyBroker, api_version: APIVersion, ) -> ModuleContext: """Get a generic module context with its dependencies mocked out.""" diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index ca28d795a39..1cabbc3fe20 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -10,7 +10,7 @@ from opentrons.types import Mount, DeckSlotName from opentrons.protocol_api import OFF_DECK -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules.types import ModuleType, TemperatureModuleModel from opentrons.protocols.api_support import instrument as mock_instrument_support from opentrons.protocols.api_support.types import APIVersion @@ -560,7 +560,7 @@ def test_move_labware_to_module( """It should move labware to new module location.""" mock_labware_core = decoy.mock(cls=LabwareCore) mock_module_core = decoy.mock(cls=TemperatureModuleCore) - mock_broker = decoy.mock(cls=Broker) + mock_broker = decoy.mock(cls=LegacyBroker) decoy.when(mock_labware_core.get_well_columns()).then_return([]) diff --git a/api/tests/opentrons/protocol_api/test_temperature_module_context.py b/api/tests/opentrons/protocol_api/test_temperature_module_context.py index ade48b8af1c..2c4c3c6b8d4 100644 --- a/api/tests/opentrons/protocol_api/test_temperature_module_context.py +++ b/api/tests/opentrons/protocol_api/test_temperature_module_context.py @@ -2,7 +2,7 @@ import pytest from decoy import Decoy, matchers -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules import TemperatureStatus from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError @@ -30,9 +30,9 @@ def mock_core_map(decoy: Decoy) -> LoadedCoreMap: @pytest.fixture -def mock_broker(decoy: Decoy) -> Broker: +def mock_broker(decoy: Decoy) -> LegacyBroker: """Get a mock command message broker.""" - return decoy.mock(cls=Broker) + return decoy.mock(cls=LegacyBroker) @pytest.fixture @@ -47,7 +47,7 @@ def subject( mock_core: TemperatureModuleCore, mock_protocol_core: ProtocolCore, mock_core_map: LoadedCoreMap, - mock_broker: Broker, + mock_broker: LegacyBroker, ) -> TemperatureModuleContext: """Get a temperature module context with its dependencies mocked out.""" return TemperatureModuleContext( @@ -62,7 +62,7 @@ def subject( def test_set_temperature( decoy: Decoy, mock_core: TemperatureModuleCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: TemperatureModuleContext, ) -> None: """It should set and wait for the temperature via the core.""" @@ -92,7 +92,7 @@ def test_set_temperature( def test_start_set_temperature( decoy: Decoy, mock_core: TemperatureModuleCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: TemperatureModuleContext, ) -> None: """It should set the target temperature via the core.""" @@ -120,7 +120,7 @@ def test_start_set_temperature( @pytest.mark.parametrize("api_version", [APIVersion(2, 2)]) def test_start_set_temperature_api_version_low( - decoy: Decoy, mock_broker: Broker, subject: TemperatureModuleContext + decoy: Decoy, mock_broker: LegacyBroker, subject: TemperatureModuleContext ) -> None: """It should reject if API version is lower than 2.3.""" with pytest.raises(APIVersionError) as exc_info: @@ -138,7 +138,7 @@ def test_start_set_temperature_api_version_low( def test_await_temperature( decoy: Decoy, mock_core: TemperatureModuleCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: TemperatureModuleContext, ) -> None: """It should wait for a specified target temperature.""" @@ -162,7 +162,7 @@ def test_await_temperature( @pytest.mark.parametrize("api_version", [APIVersion(2, 2)]) def test_await_temperature_api_version_low( - decoy: Decoy, mock_broker: Broker, subject: TemperatureModuleContext + decoy: Decoy, mock_broker: LegacyBroker, subject: TemperatureModuleContext ) -> None: """It should reject if API version is lower than 2.3.""" with pytest.raises(APIVersionError) as exc_info: @@ -179,7 +179,7 @@ def test_await_temperature_api_version_low( def test_deactivate( decoy: Decoy, mock_core: TemperatureModuleCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: TemperatureModuleContext, ) -> None: """It should deactivate the heater.""" diff --git a/api/tests/opentrons/protocol_api/test_thermocycler_context.py b/api/tests/opentrons/protocol_api/test_thermocycler_context.py index fcf08126a3c..d8f5feed1ba 100644 --- a/api/tests/opentrons/protocol_api/test_thermocycler_context.py +++ b/api/tests/opentrons/protocol_api/test_thermocycler_context.py @@ -3,7 +3,7 @@ import pytest from decoy import Decoy, matchers -from opentrons.broker import Broker +from opentrons.legacy_broker import LegacyBroker from opentrons.drivers.types import ThermocyclerLidStatus from opentrons.hardware_control.modules import TemperatureStatus from opentrons.protocols.api_support.types import APIVersion @@ -41,9 +41,9 @@ def mock_core_map(decoy: Decoy) -> LoadedCoreMap: @pytest.fixture -def mock_broker(decoy: Decoy) -> Broker: +def mock_broker(decoy: Decoy) -> LegacyBroker: """Get a mock command message broker.""" - return decoy.mock(cls=Broker) + return decoy.mock(cls=LegacyBroker) @pytest.fixture @@ -58,7 +58,7 @@ def subject( mock_core: ThermocyclerCore, mock_protocol_core: ProtocolCore, mock_core_map: LoadedCoreMap, - mock_broker: Broker, + mock_broker: LegacyBroker, ) -> ThermocyclerContext: """Get a thermocycler module context with its dependencies mocked out.""" return ThermocyclerContext( @@ -200,7 +200,7 @@ def test_get_current_step_index( def test_open_lid( decoy: Decoy, mock_core: ThermocyclerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: ThermocyclerContext, ) -> None: """It should open the lid via the core.""" @@ -231,7 +231,7 @@ def test_open_lid( def test_close_lid( decoy: Decoy, mock_core: ThermocyclerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: ThermocyclerContext, ) -> None: """It should close the lid via the core.""" @@ -262,7 +262,7 @@ def test_close_lid( def test_set_block_temperature( decoy: Decoy, mock_core: ThermocyclerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: ThermocyclerContext, ) -> None: """It should set the block temperature via the core.""" @@ -314,7 +314,7 @@ def test_set_block_temperature( def test_set_lid_temperature( decoy: Decoy, mock_core: ThermocyclerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: ThermocyclerContext, ) -> None: """It should close the lid via the core.""" @@ -342,7 +342,7 @@ def test_set_lid_temperature( def test_execute_profile( decoy: Decoy, mock_core: ThermocyclerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: ThermocyclerContext, ) -> None: """It should execute a thermocycler profile via the core.""" @@ -425,7 +425,7 @@ def test_execute_profile( def test_deactivate_lid( decoy: Decoy, mock_core: ThermocyclerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: ThermocyclerContext, ) -> None: """It should turn off the heated lid via the core.""" @@ -452,7 +452,7 @@ def test_deactivate_lid( def test_deactivate_block( decoy: Decoy, mock_core: ThermocyclerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: ThermocyclerContext, ) -> None: """It should turn off the well block temperature controller via the core.""" @@ -479,7 +479,7 @@ def test_deactivate_block( def test_deactivate( decoy: Decoy, mock_core: ThermocyclerCore, - mock_broker: Broker, + mock_broker: LegacyBroker, subject: ThermocyclerContext, ) -> None: """It should turn off the well block and heated lid via the core.""" diff --git a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py index bb7ae8ec427..ebd7c01b995 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py @@ -5,14 +5,14 @@ from datetime import datetime from typing import Callable -from opentrons.broker import Broker -from opentrons.equipment_broker import EquipmentBroker from opentrons.commands.types import CommandMessage as LegacyCommand, PauseMessage from opentrons.protocol_engine import ( StateView, actions as pe_actions, commands as pe_commands, ) +from opentrons.legacy_broker import LegacyBroker +from opentrons.util.broker import Broker from opentrons.protocol_runner.legacy_command_mapper import LegacyCommandMapper from opentrons.protocol_runner.legacy_context_plugin import LegacyContextPlugin @@ -29,15 +29,15 @@ @pytest.fixture -def mock_broker(decoy: Decoy) -> Broker: +def mock_legacy_broker(decoy: Decoy) -> LegacyBroker: """Get a mocked out LegacyProtocolContext dependency.""" - return decoy.mock(cls=Broker) + return decoy.mock(cls=LegacyBroker) @pytest.fixture -def mock_equipment_broker(decoy: Decoy) -> EquipmentBroker[LegacyLoadInfo]: +def mock_broker(decoy: Decoy) -> Broker[LegacyLoadInfo]: """Get a mocked out LegacyProtocolContext dependency.""" - return decoy.mock(cls=EquipmentBroker) + return decoy.mock(cls=Broker[LegacyLoadInfo]) @pytest.fixture @@ -60,16 +60,16 @@ def mock_action_dispatcher(decoy: Decoy) -> pe_actions.ActionDispatcher: @pytest.fixture def subject( - mock_broker: Broker, - mock_equipment_broker: EquipmentBroker[LegacyLoadInfo], + mock_legacy_broker: LegacyBroker, + mock_broker: Broker[LegacyLoadInfo], mock_legacy_command_mapper: LegacyCommandMapper, mock_state_view: StateView, mock_action_dispatcher: pe_actions.ActionDispatcher, ) -> LegacyContextPlugin: """Get a configured LegacyContextPlugin with its dependencies mocked out.""" plugin = LegacyContextPlugin( - broker=mock_broker, - equipment_broker=mock_equipment_broker, + broker=mock_legacy_broker, + equipment_broker=mock_broker, legacy_command_mapper=mock_legacy_command_mapper, ) plugin._configure(state=mock_state_view, action_dispatcher=mock_action_dispatcher) @@ -78,8 +78,8 @@ def subject( async def test_broker_subscribe_unsubscribe( decoy: Decoy, - mock_broker: Broker, - mock_equipment_broker: EquipmentBroker[LegacyLoadInfo], + mock_legacy_broker: LegacyBroker, + mock_broker: Broker[LegacyLoadInfo], subject: LegacyContextPlugin, ) -> None: """It should subscribe to the brokers on setup and unsubscribe on teardown.""" @@ -87,12 +87,12 @@ async def test_broker_subscribe_unsubscribe( equipment_broker_unsubscribe: Callable[[], None] = decoy.mock() decoy.when( - mock_broker.subscribe(topic="command", handler=matchers.Anything()) + mock_legacy_broker.subscribe(topic="command", handler=matchers.Anything()) ).then_return(command_broker_unsubscribe) - decoy.when( - mock_equipment_broker.subscribe(callback=matchers.Anything()) - ).then_return(equipment_broker_unsubscribe) + decoy.when(mock_broker.subscribe(callback=matchers.Anything())).then_return( + equipment_broker_unsubscribe + ) subject.setup() await subject.teardown() @@ -103,8 +103,8 @@ async def test_broker_subscribe_unsubscribe( async def test_command_broker_messages( decoy: Decoy, - mock_broker: Broker, - mock_equipment_broker: EquipmentBroker[LegacyLoadInfo], + mock_legacy_broker: LegacyBroker, + mock_broker: Broker[LegacyLoadInfo], mock_legacy_command_mapper: LegacyCommandMapper, mock_action_dispatcher: pe_actions.ActionDispatcher, subject: LegacyContextPlugin, @@ -115,11 +115,11 @@ async def test_command_broker_messages( # (instead of Decoy's default `None`) so subject.teardown() works. command_handler_captor = matchers.Captor() decoy.when( - mock_broker.subscribe(topic="command", handler=command_handler_captor) - ).then_return(decoy.mock()) - decoy.when( - mock_equipment_broker.subscribe(callback=matchers.Anything()) + mock_legacy_broker.subscribe(topic="command", handler=command_handler_captor) ).then_return(decoy.mock()) + decoy.when(mock_broker.subscribe(callback=matchers.Anything())).then_return( + decoy.mock() + ) subject.setup() @@ -157,8 +157,8 @@ async def test_command_broker_messages( async def test_equipment_broker_messages( decoy: Decoy, - mock_broker: Broker, - mock_equipment_broker: EquipmentBroker[LegacyLoadInfo], + mock_legacy_broker: LegacyBroker, + mock_broker: Broker[LegacyLoadInfo], mock_legacy_command_mapper: LegacyCommandMapper, mock_action_dispatcher: pe_actions.ActionDispatcher, subject: LegacyContextPlugin, @@ -170,11 +170,11 @@ async def test_equipment_broker_messages( # (instead of Decoy's default `None`) so subject.teardown() works. labware_handler_captor = matchers.Captor() decoy.when( - mock_broker.subscribe(topic="command", handler=matchers.Anything()) - ).then_return(decoy.mock()) - decoy.when( - mock_equipment_broker.subscribe(callback=labware_handler_captor) + mock_legacy_broker.subscribe(topic="command", handler=matchers.Anything()) ).then_return(decoy.mock()) + decoy.when(mock_broker.subscribe(callback=labware_handler_captor)).then_return( + decoy.mock() + ) subject.setup() diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index aa9e36d5740..8e31384311f 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -5,19 +5,20 @@ from pathlib import Path from typing import List, cast, Optional, Union, Type +from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.protocol.models import ProtocolSchemaV6, ProtocolSchemaV7 from opentrons_shared_data.protocol.dev_types import ( JsonProtocol as LegacyJsonProtocolDict, ) -from opentrons.broker import Broker -from opentrons.equipment_broker import EquipmentBroker from opentrons.hardware_control import API as HardwareAPI +from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_engine.types import PostRunHardwareState from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.parse import PythonParseMode -from opentrons_shared_data.protocol.models import ProtocolSchemaV6, ProtocolSchemaV7 -from opentrons_shared_data.labware.labware_definition import LabwareDefinition -from opentrons.protocol_engine import ProtocolEngine, Liquid, commands as pe_commands +from opentrons.util.broker import Broker + from opentrons import protocol_reader +from opentrons.protocol_engine import ProtocolEngine, Liquid, commands as pe_commands from opentrons.protocol_reader import ( ProtocolSource, JsonProtocolConfig, @@ -446,8 +447,8 @@ async def test_load_legacy_python( decoy.when( legacy_context_creator.create( protocol=legacy_protocol, - broker=matchers.IsA(Broker), - equipment_broker=matchers.IsA(EquipmentBroker), + broker=matchers.IsA(LegacyBroker), + equipment_broker=matchers.IsA(Broker), ) ).then_return(legacy_context) @@ -574,8 +575,8 @@ async def test_load_legacy_json( decoy.when( legacy_context_creator.create( legacy_protocol, - broker=matchers.IsA(Broker), - equipment_broker=matchers.IsA(EquipmentBroker), + broker=matchers.IsA(LegacyBroker), + equipment_broker=matchers.IsA(Broker), ) ).then_return(legacy_context) diff --git a/api/tests/opentrons/broker/test_broker.py b/api/tests/opentrons/test_legacy_broker.py similarity index 83% rename from api/tests/opentrons/broker/test_broker.py rename to api/tests/opentrons/test_legacy_broker.py index 70356e052ce..2351f73e348 100644 --- a/api/tests/opentrons/broker/test_broker.py +++ b/api/tests/opentrons/test_legacy_broker.py @@ -1,10 +1,12 @@ +"""Tests for `LegacyBroker`.""" + from typing import List, NamedTuple, cast from opentrons.commands.types import CommandMessage from opentrons.commands.publisher import CommandPublisher, publish -def my_command(arg1: int, arg2: str = "", arg3: str = "") -> CommandMessage: +def _my_command(arg1: int, arg2: str = "", arg3: str = "") -> CommandMessage: return cast( CommandMessage, { @@ -19,29 +21,30 @@ class _Call(NamedTuple): description: str -class FakeClass(CommandPublisher): +class _FakeClass(CommandPublisher): def __init__(self) -> None: super().__init__(None) - @publish(command=my_command) + @publish(command=_my_command) def method_a(self, arg1: int, arg2: str, arg3: str = "foo") -> int: self.method_b(0) return 100 - @publish(command=my_command) + @publish(command=_my_command) def method_b(self, arg1: int) -> None: return None - @publish(command=my_command) + @publish(command=_my_command) def method_c(self, arg1: int, arg2: str, arg3: str = "bar") -> int: self.method_b(0) return 100 -def test_add_listener() -> None: +def test_legacy_broker() -> None: + """Test subscribing, emitting, receiving, and unsubscribing.""" stack: List[CommandMessage] = [] calls: List[_Call] = [] - fake_obj = FakeClass() + fake_obj = _FakeClass() def on_notify(message: CommandMessage) -> None: assert message["name"] == "command" # type: ignore[comparison-overlap] diff --git a/api/tests/opentrons/util/__init__.py b/api/tests/opentrons/util/__init__.py index e69de29bb2d..4109783706b 100644 --- a/api/tests/opentrons/util/__init__.py +++ b/api/tests/opentrons/util/__init__.py @@ -0,0 +1 @@ +"""Tests for `opentrons.util`.""" diff --git a/api/tests/opentrons/util/test_broker.py b/api/tests/opentrons/util/test_broker.py new file mode 100644 index 00000000000..fc2abb8c2dc --- /dev/null +++ b/api/tests/opentrons/util/test_broker.py @@ -0,0 +1,33 @@ +"""Unit tests for `opentrons.util.broker`.""" + + +from typing import List + +from opentrons.util.broker import Broker + + +def test_broker() -> None: + """Test subscribing, receiving messages, and unsubscribing.""" + subject = Broker[int]() + + received_by_a: List[int] = [] + received_by_b: List[int] = [] + + def callback_a(message: int) -> None: + received_by_a.append(message) + + def callback_b(message: int) -> None: + received_by_b.append(message) + + subject.publish(1) + unsubscribe_a = subject.subscribe(callback_a) + subject.publish(2) + unsubscribe_b = subject.subscribe(callback_b) + subject.publish(3) + unsubscribe_b() + subject.publish(4) + unsubscribe_a() + subject.publish(5) + + assert received_by_a == [2, 3, 4] + assert received_by_b == [3] From c9866a7bce6578f9626ee054c7ad6187ad797730 Mon Sep 17 00:00:00 2001 From: Jamey H Date: Thu, 28 Sep 2023 12:42:42 -0400 Subject: [PATCH 08/79] fix(app): correct various displayed text inconsistencies (#13677) Closes RQA-1648, RQA-1649, RQA-1646, RQA-1652, RQA-1645 Fixes various punctuation, casing, and missing errors on the desktop app/ODD. --- .../localization/en/device_settings.json | 2 +- .../en/labware_position_check.json | 2 +- .../organisms/Devices/PipetteCard/index.tsx | 16 ++++++++++-- .../SetupLabwarePositionCheck.test.tsx | 26 ++++++++++++++++++- .../SetupLabwarePositionCheck/index.tsx | 14 ++++++++-- .../__tests__/RenameRobotSlideout.test.tsx | 8 +++--- app/src/organisms/GripperCard/index.tsx | 12 +++++++++ .../organisms/GripperWizardFlows/Success.tsx | 11 ++++++++ .../IntroScreen/index.tsx | 10 +++++-- .../LabwarePositionCheckComponent.tsx | 8 +++++- .../RobotMotionLoader.tsx | 8 ++++++ .../organisms/LabwarePositionCheck/index.tsx | 2 +- .../LabwarePositionCheck/useLaunchLPC.tsx | 5 +++- .../__tests__/ProtocolSetup.test.tsx | 2 +- .../OnDeviceDisplay/ProtocolSetup/index.tsx | 2 +- .../__tests__/NameRobot.test.tsx | 2 +- 16 files changed, 111 insertions(+), 19 deletions(-) diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index c74b84f2dd1..554f1c2aadc 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -164,7 +164,7 @@ "name_love_it": "{{name}}, love it!", "name_rule_description": "Enter up to 17 characters (letters and numbers only)", "name_rule_error_exist": "Oops! Name is already in use. Choose a different name.", - "name_rule_error_name_length": "Oops! Robot name must follow the character count and limitations", + "name_rule_error_name_length": "Oops! Robot name must follow the character count and limitations.", "name_rule_error_too_short": "Oops! Too short. Robot name must be at least 1 character.", "name_your_robot_description": "Don’t worry, you can always change this in your settings.", "name_your_robot": "Name your robot", diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json index e0fa1ea9fe9..d18277365fb 100644 --- a/app/src/assets/localization/en/labware_position_check.json +++ b/app/src/assets/localization/en/labware_position_check.json @@ -2,7 +2,7 @@ "adapter_in_mod_in_slot": "{{adapter}} in {{module}} in {{slot}}", "adapter_in_slot": "{{adapter}} in {{slot}}", "adapter_in_tc": "{{adapter}} in {{module}}", - "all_modules_and_labware_from_protocol": "All modules and labware used in the protocol", + "all_modules_and_labware_from_protocol": "All modules and labware used in the protocol {{protocol_name}}", "applied_offset_data": "Applied Labware Offset data", "apply_offset_data": "Apply labware offset data", "apply_offsets": "apply offsets", diff --git a/app/src/organisms/Devices/PipetteCard/index.tsx b/app/src/organisms/Devices/PipetteCard/index.tsx index d9fe1f693aa..6cb41f0560b 100644 --- a/app/src/organisms/Devices/PipetteCard/index.tsx +++ b/app/src/organisms/Devices/PipetteCard/index.tsx @@ -63,11 +63,22 @@ interface PipetteCardProps { updatePipette: () => void pipetteId?: AttachedPipette['id'] | null } -const BANNER_LINK_CSS = css` +const BANNER_LINK_STYLE = css` text-decoration: underline; cursor: pointer; margin-left: ${SPACING.spacing8}; ` + +const INSTRUMENT_CARD_STYLE = css` + p { + text-transform: lowercase; + } + + p::first-letter { + text-transform: uppercase; + } +` + const SUBSYSTEM_UPDATE_POLL_MS = 5000 export const PipetteCard = (props: PipetteCardProps): JSX.Element => { @@ -294,6 +305,7 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { {(pipetteIsBad || subsystemUpdateData != null) && ( { updateLink: ( ), 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 c851f51ae44..e6a861009cf 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx @@ -5,6 +5,11 @@ import { fireEvent } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../../../i18n' +import { + useRunQuery, + useProtocolQuery, + useProtocolAnalysisAsDocumentQuery, +} from '@opentrons/react-api-client' import { useLPCSuccessToast } from '../../../hooks/useLPCSuccessToast' import { getModuleTypesThatRequireExtraAttention } from '../../utils/getModuleTypesThatRequireExtraAttention' import { useLaunchLPC } from '../../../../LabwarePositionCheck/useLaunchLPC' @@ -24,6 +29,7 @@ jest.mock('../../../../RunTimeControl/hooks') jest.mock('../../../../../redux/config') jest.mock('../../../hooks') jest.mock('../../../hooks/useLPCSuccessToast') +jest.mock('@opentrons/react-api-client') const mockGetModuleTypesThatRequireExtraAttention = getModuleTypesThatRequireExtraAttention as jest.MockedFunction< typeof getModuleTypesThatRequireExtraAttention @@ -52,6 +58,13 @@ const mockUseLPCDisabledReason = useLPCDisabledReason as jest.MockedFunction< const mockUseLaunchLPC = useLaunchLPC as jest.MockedFunction< typeof useLaunchLPC > +const mockUseRunQuery = useRunQuery as jest.MockedFunction +const mockUseProtocolQuery = useProtocolQuery as jest.MockedFunction< + typeof useProtocolQuery +> +const mockUseProtocolAnalysisAsDocumentQuery = useProtocolAnalysisAsDocumentQuery as jest.MockedFunction< + typeof useProtocolAnalysisAsDocumentQuery +> const DISABLED_REASON = 'MOCK_DISABLED_REASON' const ROBOT_NAME = 'otie' const RUN_ID = '1' @@ -150,11 +163,22 @@ describe('SetupLabware', () => { when(mockGetIsLabwareOffsetCodeSnippetsOn).mockReturnValue(false) when(mockUseLPCDisabledReason).mockReturnValue(null) when(mockUseLaunchLPC) - .calledWith(RUN_ID) + .calledWith(RUN_ID, 'test protocol') .mockReturnValue({ launchLPC: mockLaunchLPC, LPCWizard:
mock LPC Wizard
, }) + when(mockUseRunQuery).mockReturnValue({ + data: { + data: { protocolId: 'fakeProtocolId' }, + }, + } as any) + when(mockUseProtocolQuery).mockReturnValue({ + data: { data: { metadata: { protocolName: 'test protocol' } } }, + } as any) + when(mockUseProtocolAnalysisAsDocumentQuery).mockReturnValue({ + data: null, + } as any) }) afterEach(() => { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index c25a88f7202..7df13a85fcf 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -13,7 +13,7 @@ import { PrimaryButton, COLORS, } from '@opentrons/components' -import { useRunQuery } from '@opentrons/react-api-client' +import { useRunQuery, useProtocolQuery } from '@opentrons/react-api-client' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useLPCSuccessToast } from '../../hooks/useLPCSuccessToast' import { Tooltip } from '../../../../atoms/Tooltip' @@ -36,6 +36,16 @@ export function SetupLabwarePositionCheck( const { t, i18n } = useTranslation('protocol_setup') const { data: runRecord } = useRunQuery(runId, { staleTime: Infinity }) + const { data: protocolRecord } = useProtocolQuery( + runRecord?.data.protocolId ?? null, + { + staleTime: Infinity, + } + ) + const protocolName = + protocolRecord?.data.metadata.protocolName ?? + protocolRecord?.data.files[0].name ?? + '' const currentOffsets = runRecord?.data?.labwareOffsets ?? [] const sortedOffsets: LabwareOffset[] = currentOffsets.length > 0 @@ -62,7 +72,7 @@ export function SetupLabwarePositionCheck( const { setIsShowingLPCSuccessToast } = useLPCSuccessToast() - const { launchLPC, LPCWizard } = useLaunchLPC(runId) + const { launchLPC, LPCWizard } = useLaunchLPC(runId, protocolName) return ( { expect(input).toHaveValue('mockInput@@@') const renameButton = getByRole('button', { name: 'Rename robot' }) const error = await findByText( - 'Oops! Robot name must follow the character count and limitations' + 'Oops! Robot name must follow the character count and limitations.' ) await waitFor(() => { expect(renameButton).toBeDisabled() @@ -144,7 +144,7 @@ describe('RobotSettings RenameRobotSlideout', () => { expect(input).toHaveValue('aaaaaaaaaaaaaaaaaa') const renameButton = getByRole('button', { name: 'Rename robot' }) const error = await findByText( - 'Oops! Robot name must follow the character count and limitations' + 'Oops! Robot name must follow the character count and limitations.' ) await waitFor(() => { expect(renameButton).toBeDisabled() @@ -161,7 +161,7 @@ describe('RobotSettings RenameRobotSlideout', () => { expect(input).toHaveValue('Hello world123') const renameButton = getByRole('button', { name: 'Rename robot' }) const error = await findByText( - 'Oops! Robot name must follow the character count and limitations' + 'Oops! Robot name must follow the character count and limitations.' ) await waitFor(() => { expect(renameButton).toBeDisabled() @@ -178,7 +178,7 @@ describe('RobotSettings RenameRobotSlideout', () => { expect(input).toHaveValue(' ') const renameButton = getByRole('button', { name: 'Rename robot' }) const error = await findByText( - 'Oops! Robot name must follow the character count and limitations' + 'Oops! Robot name must follow the character count and limitations.' ) await waitFor(() => { expect(renameButton).toBeDisabled() diff --git a/app/src/organisms/GripperCard/index.tsx b/app/src/organisms/GripperCard/index.tsx index 0fd1871ab6e..0c900e88576 100644 --- a/app/src/organisms/GripperCard/index.tsx +++ b/app/src/organisms/GripperCard/index.tsx @@ -23,6 +23,17 @@ const BANNER_LINK_CSS = css` cursor: pointer; margin-left: ${SPACING.spacing8}; ` + +const INSTRUMENT_CARD_STYLE = css` + p { + text-transform: lowercase; + } + + p::first-letter { + text-transform: uppercase; + } +` + const SUBSYSTEM_UPDATE_POLL_MS = 5000 export function GripperCard({ @@ -125,6 +136,7 @@ export function GripperCard({ {attachedGripper?.ok === false || subsystemUpdateData != null ? ( & Pick & @@ -66,6 +76,7 @@ export const Success = ( iconColor={COLORS.successEnabled} header={header} isSuccess + css={HEADER_STYLE} > {isOnDevice ? ( diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx index 040f75b9833..b0e112154fe 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx @@ -51,6 +51,7 @@ export const IntroScreen = (props: { setFatalError: (errorMessage: string) => void isRobotMoving: boolean existingOffsets: LabwareOffset[] + protocolName: string }): JSX.Element | null => { const { proceed, @@ -59,6 +60,7 @@ export const IntroScreen = (props: { isRobotMoving, setFatalError, existingOffsets, + protocolName, } = props const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation(['labware_position_check', 'shared']) @@ -92,8 +94,12 @@ export const IntroScreen = (props: { diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx index f774cb2a878..67be4d2a1bd 100644 --- a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -46,6 +46,7 @@ interface LabwarePositionCheckModalProps { maintenanceRunId: string mostRecentAnalysis: CompletedProtocolAnalysis | null existingOffsets: LabwareOffset[] + protocolName: string caughtError?: Error setMaintenanceRunId: (id: string | null) => void } @@ -60,6 +61,7 @@ export const LabwarePositionCheckComponent = ( runId, maintenanceRunId, setMaintenanceRunId, + protocolName, } = props const { t } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) @@ -323,7 +325,11 @@ export const LabwarePositionCheckComponent = ( ) } else if (currentStep.section === 'BEFORE_BEGINNING') { modalContent = ( - + ) } else if ( currentStep.section === 'CHECK_POSITIONS' || diff --git a/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx b/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx index 40317b4056a..04a8fb62ce5 100644 --- a/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx +++ b/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx @@ -44,6 +44,14 @@ export function RobotMotionLoader(props: RobotMotionLoaderProps): JSX.Element { const LoadingText = styled.h1` ${TYPOGRAPHY.h1Default} + p { + text-transform: lowercase; + } + + p::first-letter { + text-transform: uppercase; + } + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { ${TYPOGRAPHY.level4HeaderSemiBold} } diff --git a/app/src/organisms/LabwarePositionCheck/index.tsx b/app/src/organisms/LabwarePositionCheck/index.tsx index 92e8ede4060..084384321b3 100644 --- a/app/src/organisms/LabwarePositionCheck/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/index.tsx @@ -12,6 +12,7 @@ interface LabwarePositionCheckModalProps { maintenanceRunId: string existingOffsets: LabwareOffset[] mostRecentAnalysis: CompletedProtocolAnalysis | null + protocolName: string caughtError?: Error setMaintenanceRunId: (id: string | null) => void } @@ -24,7 +25,6 @@ export const LabwarePositionCheck = ( props: LabwarePositionCheckModalProps ): JSX.Element => { const logger = useLogger(__filename) - return ( void; LPCWizard: JSX.Element | null } { const { data: runRecord } = useRunQuery(runId, { staleTime: Infinity }) + const { createTargetedMaintenanceRun, } = useCreateTargetedMaintenanceRunMutation() @@ -71,6 +73,7 @@ export function useLaunchLPC( existingOffsets={runRecord?.data?.labwareOffsets ?? []} maintenanceRunId={maintenanceRunId} setMaintenanceRunId={setMaintenanceRunId} + protocolName={protocolName ?? ''} /> ) : null, } diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 532d350d0e9..7368d3b6b20 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -269,7 +269,7 @@ describe('ProtocolSetup', () => { .calledWith() .mockReturnValue({ data: { data: [] } } as any) when(mockUseLaunchLPC) - .calledWith(RUN_ID) + .calledWith(RUN_ID, PROTOCOL_NAME) .mockReturnValue({ launchLPC: mockLaunchLPC, LPCWizard:
mock LPC Wizard
, diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx index 84356c1e1ce..1983d5dea31 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx @@ -359,7 +359,7 @@ function PrepareToRun({ protocolRecord?.data.files[0].name ?? '' const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const { launchLPC, LPCWizard } = useLaunchLPC(runId) + const { launchLPC, LPCWizard } = useLaunchLPC(runId, protocolName) const onConfirmCancelClose = (): void => { setShowConfirmCancelModal(false) diff --git a/app/src/pages/OnDeviceDisplay/__tests__/NameRobot.test.tsx b/app/src/pages/OnDeviceDisplay/__tests__/NameRobot.test.tsx index 609e2b57518..5524c927ed9 100644 --- a/app/src/pages/OnDeviceDisplay/__tests__/NameRobot.test.tsx +++ b/app/src/pages/OnDeviceDisplay/__tests__/NameRobot.test.tsx @@ -90,7 +90,7 @@ describe('NameRobot', () => { const [{ findByText, getByLabelText }] = render() getByLabelText('SmallButton_primary').click() const error = await findByText( - 'Oops! Robot name must follow the character count and limitations' + 'Oops! Robot name must follow the character count and limitations.' ) await waitFor(() => { expect(error).toBeInTheDocument() From b177328c7f40f6fc6b113c7eafff2767fd54be38 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:59:33 -0400 Subject: [PATCH 09/79] fix(app): OT-2 pipette flow success img resize (#13641) closes RQA-1214 --- app/src/molecules/SimpleWizardBody/index.tsx | 8 ++++++-- app/src/organisms/ChangePipette/ConfirmPipette.tsx | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/molecules/SimpleWizardBody/index.tsx b/app/src/molecules/SimpleWizardBody/index.tsx index 52bde1e1c8c..cc5b35ebae6 100644 --- a/app/src/molecules/SimpleWizardBody/index.tsx +++ b/app/src/molecules/SimpleWizardBody/index.tsx @@ -16,10 +16,12 @@ import { POSITION_ABSOLUTE, JUSTIFY_FLEX_START, } from '@opentrons/components' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import SuccessIcon from '../../assets/images/icon_success.png' import { getIsOnDevice } from '../../redux/config' import { StyledText } from '../../atoms/text' import { Skeleton } from '../../atoms/Skeleton' +import type { RobotType } from '@opentrons/shared-data' interface Props extends StyleProps { iconColor: string @@ -28,6 +30,7 @@ interface Props extends StyleProps { children?: React.ReactNode subHeader?: string | JSX.Element isPending?: boolean + robotType?: RobotType /** * this prop is to change justifyContent of OnDeviceDisplay buttons * TODO(jr, 8/9/23): this SHOULD be refactored so the @@ -87,6 +90,7 @@ export function SimpleWizardBody(props: Props): JSX.Element { subHeader, isSuccess, isPending, + robotType = FLEX_ROBOT_TYPE, ...styleProps } = props const isOnDevice = useSelector(getIsOnDevice) @@ -144,8 +148,8 @@ export function SimpleWizardBody(props: Props): JSX.Element { <> {isSuccess ? ( Success Icon diff --git a/app/src/organisms/ChangePipette/ConfirmPipette.tsx b/app/src/organisms/ChangePipette/ConfirmPipette.tsx index ea44722b065..eb465d058b4 100644 --- a/app/src/organisms/ChangePipette/ConfirmPipette.tsx +++ b/app/src/organisms/ChangePipette/ConfirmPipette.tsx @@ -13,10 +13,11 @@ import { CheckPipettesButton } from './CheckPipettesButton' import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' import { LevelPipette } from './LevelPipette' -import type { +import { PipetteNameSpecs, PipetteModelSpecs, PipetteDisplayCategory, + OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import type { PipetteOffsetCalibration } from '../../redux/calibration/types' import type { Mount } from '../../redux/pipettes/types' @@ -129,6 +130,7 @@ export function ConfirmPipette(props: ConfirmPipetteProps): JSX.Element { header={header} subHeader={subHeader} isSuccess={success || isWrongWantedPipette || confirmPipetteLevel} + robotType={OT2_ROBOT_TYPE} > <> {!success && !wrongWantedPipette && !confirmPipetteLevel && ( From 1665a9a274afebab645ffdd43a488c1c61c60b4a Mon Sep 17 00:00:00 2001 From: Jamey H Date: Tue, 26 Sep 2023 20:43:08 -0400 Subject: [PATCH 10/79] fix(app): render "using current calibration" banner when cancelling OT-2 deck recalibration (#13642) Closes RQA-1324 --- .../localization/en/robot_calibration.json | 29 ++++++++--------- app/src/organisms/CalibrateDeck/index.tsx | 4 +++ app/src/organisms/CalibrateDeck/types.ts | 5 +-- .../__tests__/CalibrationTaskList.test.tsx | 31 +++++++++++++++++++ .../organisms/CalibrationTaskList/index.tsx | 12 +++++-- .../__tests__/CalibrationDashboard.test.tsx | 2 +- .../hooks/useDashboardCalibrateDeck.tsx | 10 ++++-- .../Devices/CalibrationDashboard/index.tsx | 2 ++ 8 files changed, 74 insertions(+), 21 deletions(-) diff --git a/app/src/assets/localization/en/robot_calibration.json b/app/src/assets/localization/en/robot_calibration.json index 66f43d65289..44f7ca2049b 100644 --- a/app/src/assets/localization/en/robot_calibration.json +++ b/app/src/assets/localization/en/robot_calibration.json @@ -1,6 +1,7 @@ { "attached_pipettes": "attached pipette calibrations", "before_you_begin": "Before you begin", + "calibrate": "Calibrate", "calibrate_deck": "calibrate deck", "calibrate_pipette": "Calibrate pipette", "calibrate_tip_length": "Calibrate the length of a tip on this pipette.", @@ -10,17 +11,17 @@ "calibrate_z_axis_on_block": "Calibrate z-axis on block", "calibrate_z_axis_on_slot": "Calibrate z-axis in slot 5", "calibrate_z_axis_on_trash": "Calibrate z-axis on trash bin", - "calibrate": "Calibrate", "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", "calibration_complete": "Calibration complete", "calibration_dashboard": "Calibration Dashboard", + "calibration_health_check": "Calibration Health Check", "calibration_health_check_intro_body": "Calibration Health Check diagnoses problems with Deck, Tip Length, and Pipette Offset Calibration.You will move the pipettes to various positions, which will be compared against your existing calibration data.If there is a large difference, you will be prompted to redo some or all of your calibrations.", "calibration_health_check_results": "Calibration Health Check Results", - "calibration_health_check": "Calibration Health Check", "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", "calibration_recommended": "Calibration recommended", - "calibration_status_description": "For accurate and precise movement, calibrate the robot's deck, pipette offsets, and tip lengths.", "calibration_status": "Calibration Status", + "calibration_status_description": "For accurate and precise movement, calibrate the robot's deck, pipette offsets, and tip lengths.", + "calibrations_aborted": "Using current calibrations.", "calibrations_complete": "Calibrations complete!", "change_tip_rack": "Change tip rack", "check_tip_on_block": "Check tip on block", @@ -36,6 +37,7 @@ "confirm_placement": "Confirm placement", "confirm_tip_rack": "Confirm tip rack", "custom": "custom", + "deck_calibration": "Deck Calibration", "deck_calibration_description": "Calibrate the position of the robot's deck. Recommended for all new robots and after moving robots.", "deck_calibration_error_occurred": "An error occurred while trying to start deck calibration", "deck_calibration_failure": "Failed to start deck calibration", @@ -43,17 +45,16 @@ "deck_calibration_missing": "You haven't calibrated the deck yet", "deck_calibration_redo": "recalibrate deck", "deck_calibration_spinner": "Deck calibration is {{ongoing_action}}", - "deck_calibration": "Deck Calibration", "deck_invalidates_pipette_offset": "Recalibrating the deck clears pipette offset data", "definition": "Your OT-2 moves pipettes around in 3D space based on its calibration. Learn more about how calibration works on the OT-2.", "delete_calibration_data": "Delete calibration data", "did_pipette_pick_up_tip": "Did pipette pick up tip successfully?", "direction_controls": "direction controls", "do_you_have_a_cal_block": "Do you have a Calibration Block?", + "download_calibration": "Download your calibration data", "download_calibration_data_available": "Save all three types of calibration data as a JSON file.", "download_calibration_data_unavailable": "No calibration data available.", "download_calibration_title": "Download Calibration Data", - "download_calibration": "Download your calibration data", "download_details": "Download details JSON Calibration Check summary", "finish": "Finish", "get_started": "Get started", @@ -76,21 +77,21 @@ "last_calibrated": "Last calibrated", "last_completed_on": "Last completed {{timestamp}}", "last_migrated": "Last known calibration migrated", - "launch_calibration_link_text": "Go to calibration", "launch_calibration": "Launch calibration", + "launch_calibration_link_text": "Go to calibration", "manage_pipettes": "manage pipettes", - "missing_calibration_data_long": "Robot is missing calibration data", "missing_calibration_data": "Missing calibration data", + "missing_calibration_data_long": "Robot is missing calibration data", "need_help": "Need help?", "no_pipette": "No pipette attached", "no_tip_length": "Calibrate your pipette to see saved tip length", - "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", "opentrons": "opentrons", + "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", "pick_up_tip": "Pick up tip", "pipette_name_and_serial": "{{name}}, {{serial}}", + "pipette_offset_calibration": "Pipette Offset Calibration", "pipette_offset_calibration_intro_body": "Calibrating pipette offset measures a pipette’s position relative to the pipette mount and the deck.", "pipette_offset_calibration_on_mount": "Calibrate this pipette's offset while attached to the robot's {{mount}} mount.", - "pipette_offset_calibration": "Pipette Offset Calibration", "pipette_offset_description": "Calibrate the position for the the default tip and pipette combination.", "pipette_offset_recalibrate_both_mounts": "Pipette offsets for both mounts will have to be recalibrated.", "pipette_offset_requires_tip_length": "You don’t have a tip length saved with this pipette yet. You will need to calibrate tip length before calibrating your pipette offset.", @@ -100,29 +101,29 @@ "position_pipette_over_tip": "Position pipette over A1", "prepare_the_space": "Prepare the space", "progress_will_be_lost": "{{sessionType}} progress will be lost", + "recalibrate": "Recalibrate", "recalibrate_pipette": "Recalibrate pipette", "recalibrate_warning_body": "Performing a deck calibration will clear all of your pipette offset and tip length calibrations. You will need to recalibrate your pipette offset and tip length after completing a deck calibration.", "recalibrate_warning_heading": "Are you sure you want to recalibrate your deck?", - "recalibrate": "Recalibrate", "recalibration_recommended": "Recalibration recommended", + "return_tip": "Return tip", "return_tip_and_continue": "Return tip and continue to next pipette", "return_tip_and_exit": "Return tip and see calibration health check results", - "return_tip": "Return tip", "see_how_robot_calibration_works": "See how robot calibration works", "select_tip_rack": "select tip rack", "serial_number": "Serial number", "small": "Small", - "start_over_question": "Start over?", "start_over": "Start over", + "start_over_question": "Start over?", "start_with_deck_calibration": "Start with Deck Calibration, which is the basis for the rest of calibration.", "starting_over_loses_progress": "Starting over will cancel your calibration progress.", "this_is_the_tip_used_in_pipette_offset_cal": "Please note: You must use the same tips you used in Pipette Offset Calibration, which are listed above.", "tiny": "Tiny", + "tip_length": "tip length calibration", "tip_length_and_pipette_offset_calibration": "Tip Length and Pipette Offset Calibration", - "tip_length_calibration_intro_body": "Tip length calibration measures the distance between the bottom of the tip and the pipette’s nozzle.", "tip_length_calibration": "Tip Length Calibration", + "tip_length_calibration_intro_body": "Tip length calibration measures the distance between the bottom of the tip and the pipette’s nozzle.", "tip_length_invalidates_pipette_offset": "Recalibrating tip length will clear pipette offset data.", - "tip_length": "tip length calibration", "tip_pick_up_instructions": "Using the controls below or your keyboard, jog the pipette until the nozzle closest to you is centered above the A1 position and level with the top of the tip.
When the pipette is properly aligned, pick up the tip.", "title": "robot calibration", "to_check": "To check the {{mount}} pipette:", diff --git a/app/src/organisms/CalibrateDeck/index.tsx b/app/src/organisms/CalibrateDeck/index.tsx index 89b009fb9a3..8fa74d35718 100644 --- a/app/src/organisms/CalibrateDeck/index.tsx +++ b/app/src/organisms/CalibrateDeck/index.tsx @@ -65,6 +65,7 @@ export function CalibrateDeck( dispatchRequests, showSpinner, isJogging, + wasExitBeforeCompletion, offsetInvalidationHandler, } = props const { currentStep, instrument, labware, supportedCommands } = @@ -96,6 +97,9 @@ export function CalibrateDeck( } function cleanUpAndExit(): void { + if (wasExitBeforeCompletion) { + wasExitBeforeCompletion.current = true + } if (session?.id) { dispatchRequests( Sessions.createSessionCommand(robotName, session.id, { diff --git a/app/src/organisms/CalibrateDeck/types.ts b/app/src/organisms/CalibrateDeck/types.ts index 2a19f540420..fb5ae8dcb10 100644 --- a/app/src/organisms/CalibrateDeck/types.ts +++ b/app/src/organisms/CalibrateDeck/types.ts @@ -1,11 +1,12 @@ -import type { DeckCalibrationSession } from '../../redux/sessions/types' import { DispatchRequestsType } from '../../redux/robot-api' - +import type { MutableRefObject } from 'react' +import type { DeckCalibrationSession } from '../../redux/sessions/types' export interface CalibrateDeckParentProps { robotName: string session: DeckCalibrationSession | null dispatchRequests: DispatchRequestsType showSpinner: boolean isJogging: boolean + wasExitBeforeCompletion?: MutableRefObject offsetInvalidationHandler?: () => void } diff --git a/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx b/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx index 798a798f1a8..2bf0b7cfddc 100644 --- a/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx +++ b/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx @@ -43,6 +43,7 @@ const render = (robotName: string = 'otie') => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> , { @@ -93,6 +94,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> ) @@ -118,6 +120,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> ) @@ -142,6 +145,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> ) @@ -166,6 +170,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> ) @@ -189,6 +194,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> ) @@ -212,12 +218,35 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> ) expect(getByText('Calibrations complete!')).toBeTruthy() }) + it('exiting a recalibrate wizard from a task will allow the current calibrations screen to show', () => { + mockUseCalibrationTaskList.mockReturnValueOnce( + expectedIncompleteRightMountTaskList + ) + + const [{ getByText, rerender }] = render() + const recalibrateLink = getByText('Recalibrate') + recalibrateLink.click() + rerender( + + + + ) + expect(getByText('Using current calibrations.')).toBeTruthy() + }) + it('prevents the user from launching calibrations or recalibrations from a task when a protocol run is active', () => { mockUseCalibrationTaskList.mockReturnValueOnce( expectedIncompleteDeckCalTaskList @@ -236,6 +265,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> ) @@ -263,6 +293,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} + wasExitBeforeCompletion={false} /> ) diff --git a/app/src/organisms/CalibrationTaskList/index.tsx b/app/src/organisms/CalibrationTaskList/index.tsx index c1f1986f587..618ceabe68a 100644 --- a/app/src/organisms/CalibrationTaskList/index.tsx +++ b/app/src/organisms/CalibrationTaskList/index.tsx @@ -35,6 +35,7 @@ interface CalibrationTaskListProps { pipOffsetCalLauncher: DashboardCalOffsetInvoker tipLengthCalLauncher: DashboardCalTipLengthInvoker deckCalLauncher: DashboardCalDeckInvoker + wasExitBeforeCompletion: boolean } export function CalibrationTaskList({ @@ -42,6 +43,7 @@ export function CalibrationTaskList({ pipOffsetCalLauncher, tipLengthCalLauncher, deckCalLauncher, + wasExitBeforeCompletion, }: CalibrationTaskListProps): JSX.Element { const prevActiveIndex = React.useRef<[number, number] | null>(null) const [hasLaunchedWizard, setHasLaunchedWizard] = React.useState( @@ -125,9 +127,15 @@ export function CalibrationTaskList({ justifyContent={JUSTIFY_CENTER} alignItems={ALIGN_CENTER} > - + {wasExitBeforeCompletion ? ( + + ) : ( + + )} - {t('calibrations_complete')} + {wasExitBeforeCompletion + ? t('calibrations_aborted') + : t('calibrations_complete')} { mockUseCalibrationTaskList.mockReturnValue(expectedTaskList) mockUseDashboardCalibratePipOffset.mockReturnValue([() => {}, null]) mockUseDashboardCalibrateTipLength.mockReturnValue([() => {}, null]) - mockUseDashboardCalibrateDeck.mockReturnValue([() => {}, null]) + mockUseDashboardCalibrateDeck.mockReturnValue([() => {}, null, false]) mockUseAttachedPipettes.mockReturnValue({ left: mockLeftProtoPipette, right: null, diff --git a/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx b/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx index f037f8ddb5c..ebc6ef1d50f 100644 --- a/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx +++ b/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx @@ -29,10 +29,11 @@ export type DashboardCalDeckInvoker = ( export function useDashboardCalibrateDeck( robotName: string -): [DashboardCalDeckInvoker, JSX.Element | null] { +): [DashboardCalDeckInvoker, JSX.Element | null, boolean] { const trackedRequestId = React.useRef(null) const createRequestId = React.useRef(null) const jogRequestId = React.useRef(null) + const wasExitBeforeCompletion = React.useRef(false) const invalidateHandlerRef = React.useRef<(() => void) | undefined>() const { t } = useTranslation('robot_calibration') @@ -118,6 +119,7 @@ export function useDashboardCalibrateDeck( showSpinner={showSpinner} dispatchRequests={dispatchRequests} isJogging={isJogging} + wasExitBeforeCompletion={wasExitBeforeCompletion} offsetInvalidationHandler={invalidateHandlerRef.current} /> )} @@ -126,5 +128,9 @@ export function useDashboardCalibrateDeck( if (!(startingSession || deckCalSession != null)) Wizard = null - return [handleStartDashboardDeckCalSession, Wizard] + return [ + handleStartDashboardDeckCalSession, + Wizard, + wasExitBeforeCompletion.current, + ] } diff --git a/app/src/pages/Devices/CalibrationDashboard/index.tsx b/app/src/pages/Devices/CalibrationDashboard/index.tsx index 7f2b96d933d..fa13f935104 100644 --- a/app/src/pages/Devices/CalibrationDashboard/index.tsx +++ b/app/src/pages/Devices/CalibrationDashboard/index.tsx @@ -25,6 +25,7 @@ export function CalibrationDashboard(): JSX.Element { const [ dashboardDeckCalLauncher, DashboardDeckCalWizard, + wasExitBeforeCompletion, ] = useDashboardCalibrateDeck(robotName) return ( {DashboardDeckCalWizard} {DashboardOffsetCalWizard} From 885b04b606ee1f169761aad67b15b54214efc9ed Mon Sep 17 00:00:00 2001 From: Jamey H Date: Thu, 28 Sep 2023 12:48:42 -0400 Subject: [PATCH 11/79] fix(app): fix physical odd stylistic issues (#13678) Closes RQA-1513, RAUT-750, RQA-1686 Provides fixes for tearing snackbars & protocol command animations, magblock "is not connected" chip display, and flashing "firmware update" screens during wizard flows. --- app/src/atoms/Snackbar/index.tsx | 8 ++ app/src/atoms/Toast/index.tsx | 2 +- .../getGripperWizardSteps.ts | 10 ++- .../organisms/GripperWizardFlows/index.tsx | 6 +- .../getModuleCalibrationSteps.ts | 10 ++- app/src/organisms/ModuleWizardFlows/index.tsx | 12 ++- .../CurrentRunningProtocolCommand.tsx | 11 ++- .../__tests__/getPipetteWizardSteps.test.tsx | 84 +++++++++++++++++-- .../getPipetteWizardStepsForProtocol.test.tsx | 78 ++++++++++++++--- .../getPipetteWizardSteps.ts | 22 ++++- .../getPipetteWizardStepsForProtocol.ts | 38 +++++++-- .../organisms/PipetteWizardFlows/index.tsx | 13 ++- app/src/pages/Protocols/hooks/index.ts | 17 +++- 13 files changed, 264 insertions(+), 47 deletions(-) diff --git a/app/src/atoms/Snackbar/index.tsx b/app/src/atoms/Snackbar/index.tsx index 665f7a9e49b..4d37e66d696 100644 --- a/app/src/atoms/Snackbar/index.tsx +++ b/app/src/atoms/Snackbar/index.tsx @@ -22,10 +22,17 @@ export interface SnackbarProps extends StyleProps { const SNACKBAR_ANIMATION_DURATION = 500 +const ODD_ANIMATION_OPTIMIZATIONS = ` + backface-visibility: hidden; + perspective: 1000; + will-change: opacity; + ` + const OPEN_STYLE = css` animation-duration: ${SNACKBAR_ANIMATION_DURATION}ms; animation-name: fadein; overflow: hidden; + ${ODD_ANIMATION_OPTIMIZATIONS} @keyframes fadein { 0% { @@ -41,6 +48,7 @@ const CLOSE_STYLE = css` animation-duration: ${SNACKBAR_ANIMATION_DURATION}ms; animation-name: fadeout; overflow: hidden; + ${ODD_ANIMATION_OPTIMIZATIONS} @keyframes fadeout { 0% { diff --git a/app/src/atoms/Toast/index.tsx b/app/src/atoms/Toast/index.tsx index cf5f3c2f96c..7787f1cfe1d 100644 --- a/app/src/atoms/Toast/index.tsx +++ b/app/src/atoms/Toast/index.tsx @@ -103,7 +103,7 @@ export function Toast(props: ToastProps): JSX.Element { const ODD_ANIMATION_OPTIMIZATIONS = ` backface-visibility: hidden; perspective: 1000; - will-change: opacity, transform3d; + will-change: opacity, transform; ` const DESKTOP_ANIMATION_SLIDE_UP_AND_IN = css` animation-duration: ${TOAST_ANIMATION_DURATION}ms; diff --git a/app/src/organisms/GripperWizardFlows/getGripperWizardSteps.ts b/app/src/organisms/GripperWizardFlows/getGripperWizardSteps.ts index 98d22c8abdd..f7f47a24cc0 100644 --- a/app/src/organisms/GripperWizardFlows/getGripperWizardSteps.ts +++ b/app/src/organisms/GripperWizardFlows/getGripperWizardSteps.ts @@ -12,7 +12,8 @@ import { import type { GripperWizardStep, GripperWizardFlowType } from './types' export const getGripperWizardSteps = ( - flowType: GripperWizardFlowType + flowType: GripperWizardFlowType, + requiresFirmwareUpdate: boolean ): GripperWizardStep[] => { switch (flowType) { case GRIPPER_FLOW_TYPES.RECALIBRATE: { @@ -31,7 +32,7 @@ export const getGripperWizardSteps = ( ] } case GRIPPER_FLOW_TYPES.ATTACH: { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING }, { section: SECTIONS.MOUNT_GRIPPER }, { section: SECTIONS.FIRMWARE_UPDATE }, @@ -47,6 +48,10 @@ export const getGripperWizardSteps = ( successfulAction: SUCCESSFULLY_ATTACHED_AND_CALIBRATED, }, ] + + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) } case GRIPPER_FLOW_TYPES.DETACH: { return [ @@ -56,5 +61,4 @@ export const getGripperWizardSteps = ( ] } } - return [] } diff --git a/app/src/organisms/GripperWizardFlows/index.tsx b/app/src/organisms/GripperWizardFlows/index.tsx index 3910cda685c..44243e3eea0 100644 --- a/app/src/organisms/GripperWizardFlows/index.tsx +++ b/app/src/organisms/GripperWizardFlows/index.tsx @@ -209,7 +209,11 @@ export const GripperWizard = ( } = props const isOnDevice = useSelector(getIsOnDevice) const { t } = useTranslation('gripper_wizard_flows') - const gripperWizardSteps = getGripperWizardSteps(flowType) + const requiresFirmwareUpdate = !attachedGripper?.ok + const gripperWizardSteps = getGripperWizardSteps( + flowType, + requiresFirmwareUpdate + ) const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const [ frontJawOffset, diff --git a/app/src/organisms/ModuleWizardFlows/getModuleCalibrationSteps.ts b/app/src/organisms/ModuleWizardFlows/getModuleCalibrationSteps.ts index f57c8683771..510328e20f3 100644 --- a/app/src/organisms/ModuleWizardFlows/getModuleCalibrationSteps.ts +++ b/app/src/organisms/ModuleWizardFlows/getModuleCalibrationSteps.ts @@ -1,8 +1,10 @@ import { SECTIONS } from './constants' import type { ModuleCalibrationWizardStep } from './types' -export const getModuleCalibrationSteps = (): ModuleCalibrationWizardStep[] => { - return [ +export const getModuleCalibrationSteps = ( + requiresFirmwareUpdate: boolean +): ModuleCalibrationWizardStep[] => { + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING }, { section: SECTIONS.FIRMWARE_UPDATE }, { section: SECTIONS.SELECT_LOCATION }, @@ -11,4 +13,8 @@ export const getModuleCalibrationSteps = (): ModuleCalibrationWizardStep[] => { { section: SECTIONS.DETACH_PROBE }, { section: SECTIONS.SUCCESS }, ] + + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) } diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 138b09ed666..7cc6c8bde54 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -61,7 +61,13 @@ export const ModuleWizardFlows = ( const { t } = useTranslation('module_wizard_flows') const attachedPipettes = useAttachedPipettesFromInstrumentsQuery() const attachedPipette = attachedPipettes.left ?? attachedPipettes.right - const moduleCalibrationSteps = getModuleCalibrationSteps() + const requiresFirmwareUpdate = !attachedPipette?.ok + const attachedPipetteMount = + attachedPipette?.mount === LEFT ? 'pipette_left' : 'pipette_right' + + const moduleCalibrationSteps = getModuleCalibrationSteps( + requiresFirmwareUpdate + ) const availableSlotNames = FLEX_SLOT_NAMES_BY_MOD_TYPE[getModuleType(attachedModule.moduleModel)] ?? [] @@ -263,9 +269,7 @@ export const ModuleWizardFlows = ( modalContent = ( ) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx index 68e544b7a31..a551e4badff 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx @@ -36,14 +36,20 @@ import type { RunStatus } from '@opentrons/api-client' import type { TrackProtocolRunEvent } from '../../Devices/hooks' import type { RobotAnalyticsData } from '../../../redux/analytics/types' +const ODD_ANIMATION_OPTIMIZATIONS = ` + backface-visibility: hidden; + perspective: 1000; + will-change: opacity, transform; + ` + const fadeIn = keyframes` from { opacity: 0; - transform: translateY(100%); + transform: translate3d(0,15%,0); } to { opacity: 1; - transform: translateY(0%); + transform: translate3d(0,0,0); } ` @@ -79,6 +85,7 @@ const COMMAND_ROW_STYLE_ANIMATED = css` -webkit-line-clamp: 2; overflow: hidden; animation: ${fadeIn} 1.5s ease-in-out; + ${ODD_ANIMATION_OPTIMIZATIONS} ` const COMMAND_ROW_STYLE = css` diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardSteps.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardSteps.test.tsx index 26871946caf..4e9ade83185 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardSteps.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardSteps.test.tsx @@ -34,9 +34,16 @@ describe('getPipetteWizardSteps', () => { ] as PipetteWizardStep[] expect( - getPipetteWizardSteps(FLOWS.CALIBRATE, LEFT, SINGLE_MOUNT_PIPETTES, false) + getPipetteWizardSteps( + FLOWS.CALIBRATE, + LEFT, + SINGLE_MOUNT_PIPETTES, + false, + true + ) ).toStrictEqual(mockCalibrateFlowSteps) }) + it('returns the correct array of info for attach pipette flow single channel', () => { const mockAttachPipetteFlowSteps = [ { @@ -77,7 +84,58 @@ describe('getPipetteWizardSteps', () => { ] as PipetteWizardStep[] expect( - getPipetteWizardSteps(FLOWS.ATTACH, LEFT, SINGLE_MOUNT_PIPETTES, false) + getPipetteWizardSteps( + FLOWS.ATTACH, + LEFT, + SINGLE_MOUNT_PIPETTES, + false, + true + ) + ).toStrictEqual(mockAttachPipetteFlowSteps) + }) + + it('returns the correct array of info when a firmware update is not required', () => { + const mockAttachPipetteFlowSteps = [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.ATTACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.CALIBRATE, + }, + ] as PipetteWizardStep[] + + expect( + getPipetteWizardSteps( + FLOWS.ATTACH, + LEFT, + SINGLE_MOUNT_PIPETTES, + false, + false + ) ).toStrictEqual(mockAttachPipetteFlowSteps) }) @@ -101,7 +159,13 @@ describe('getPipetteWizardSteps', () => { ] as PipetteWizardStep[] expect( - getPipetteWizardSteps(FLOWS.DETACH, LEFT, SINGLE_MOUNT_PIPETTES, false) + getPipetteWizardSteps( + FLOWS.DETACH, + LEFT, + SINGLE_MOUNT_PIPETTES, + false, + true + ) ).toStrictEqual(mockDetachPipetteFlowSteps) }) @@ -155,7 +219,7 @@ describe('getPipetteWizardSteps', () => { ] as PipetteWizardStep[] expect( - getPipetteWizardSteps(FLOWS.ATTACH, LEFT, NINETY_SIX_CHANNEL, true) + getPipetteWizardSteps(FLOWS.ATTACH, LEFT, NINETY_SIX_CHANNEL, true, true) ).toStrictEqual(mockAttachPipetteFlowSteps) }) @@ -189,7 +253,7 @@ describe('getPipetteWizardSteps', () => { ] as PipetteWizardStep[] expect( - getPipetteWizardSteps(FLOWS.DETACH, LEFT, NINETY_SIX_CHANNEL, true) + getPipetteWizardSteps(FLOWS.DETACH, LEFT, NINETY_SIX_CHANNEL, true, true) ).toStrictEqual(mockDetachPipetteFlowSteps) }) it('returns the correct array when 96-channel is going to be attached and there is a pipette already on the mount', () => { @@ -248,7 +312,7 @@ describe('getPipetteWizardSteps', () => { ] as PipetteWizardStep[] expect( - getPipetteWizardSteps(FLOWS.ATTACH, LEFT, NINETY_SIX_CHANNEL, false) + getPipetteWizardSteps(FLOWS.ATTACH, LEFT, NINETY_SIX_CHANNEL, false, true) ).toStrictEqual(mockAttachPipetteFlowSteps) }) it('returns the corect array of info for calibrate pipette 96 channel', () => { @@ -276,7 +340,13 @@ describe('getPipetteWizardSteps', () => { ] as PipetteWizardStep[] expect( - getPipetteWizardSteps(FLOWS.CALIBRATE, LEFT, NINETY_SIX_CHANNEL, false) + getPipetteWizardSteps( + FLOWS.CALIBRATE, + LEFT, + NINETY_SIX_CHANNEL, + false, + true + ) ).toStrictEqual(mockCalibrateFlowSteps) }) }) diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardStepsForProtocol.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardStepsForProtocol.test.tsx index 3d1e5c09da8..6bbe76cfba0 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardStepsForProtocol.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardStepsForProtocol.test.tsx @@ -36,14 +36,20 @@ describe('getPipetteWizardStepsForProtocol', () => { mount: 'left', }, ], - LEFT + LEFT, + true ) ).toStrictEqual(mockFlowSteps) }) it('returns an empty array when there is no pipette attached and no pipette is needed', () => { const mockFlowSteps = [] as PipetteWizardStep[] expect( - getPipetteWizardStepsForProtocol({ left: null, right: null }, [], LEFT) + getPipetteWizardStepsForProtocol( + { left: null, right: null }, + [], + LEFT, + true + ) ).toStrictEqual(mockFlowSteps) }) it('returns the calibration flow only when correct pipette is attached but there is no pip cal data', () => { @@ -75,7 +81,8 @@ describe('getPipetteWizardStepsForProtocol', () => { } as any, }, [{ id: '123', pipetteName: 'p1000_single_flex', mount: 'right' }], - RIGHT + RIGHT, + true ) ).toStrictEqual(mockFlowSteps) }) @@ -123,7 +130,52 @@ describe('getPipetteWizardStepsForProtocol', () => { getPipetteWizardStepsForProtocol( mockSingleMountPipetteAttached, mockPipettesInProtocolMulti as any, - LEFT + LEFT, + true + ) + ).toStrictEqual(mockFlowSteps) + }) + it('returns the correct array of info when the attached pipette does not require a firmware update', () => { + const mockFlowSteps = [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.DETACH_PIPETTE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.DETACH }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.CALIBRATE, + }, + ] as PipetteWizardStep[] + expect( + getPipetteWizardStepsForProtocol( + mockSingleMountPipetteAttached, + mockPipettesInProtocolMulti as any, + LEFT, + false ) ).toStrictEqual(mockFlowSteps) }) @@ -181,7 +233,8 @@ describe('getPipetteWizardStepsForProtocol', () => { getPipetteWizardStepsForProtocol( { left: mock96ChannelAttachedPipetteInformation, right: null }, mockPipettesInProtocolNotEmpty as any, - LEFT + LEFT, + true ) ).toStrictEqual(mockFlowSteps) }) @@ -239,7 +292,8 @@ describe('getPipetteWizardStepsForProtocol', () => { getPipetteWizardStepsForProtocol( { left: mockAttachedPipetteInformation, right: null }, mockPipetteInfo as any, - LEFT + LEFT, + true ) ).toStrictEqual(mockFlowSteps) }) @@ -297,7 +351,8 @@ describe('getPipetteWizardStepsForProtocol', () => { getPipetteWizardStepsForProtocol( { left: null, right: mockAttachedPipetteInformation }, mockPipetteInfo as any, - LEFT + LEFT, + true ) ).toStrictEqual(mockFlowSteps) }) @@ -364,7 +419,8 @@ describe('getPipetteWizardStepsForProtocol', () => { right: mockAttachedPipetteInformation, }, mockPipetteInfo as any, - LEFT + LEFT, + true ) ).toStrictEqual(mockFlowSteps) }) @@ -406,7 +462,8 @@ describe('getPipetteWizardStepsForProtocol', () => { getPipetteWizardStepsForProtocol( { left: null, right: null }, mockPipettesInProtocolNotEmpty as any, - LEFT + LEFT, + true ) ).toStrictEqual(mockFlowSteps) }) @@ -458,7 +515,8 @@ describe('getPipetteWizardStepsForProtocol', () => { getPipetteWizardStepsForProtocol( { left: null, right: null }, mockPipetteInfo as any, - LEFT + LEFT, + true ) ).toStrictEqual(mockFlowSteps) }) diff --git a/app/src/organisms/PipetteWizardFlows/getPipetteWizardSteps.ts b/app/src/organisms/PipetteWizardFlows/getPipetteWizardSteps.ts index c67a9e07fb4..a492822e891 100644 --- a/app/src/organisms/PipetteWizardFlows/getPipetteWizardSteps.ts +++ b/app/src/organisms/PipetteWizardFlows/getPipetteWizardSteps.ts @@ -11,7 +11,8 @@ export const getPipetteWizardSteps = ( flowType: PipetteWizardFlow, mount: PipetteMount, selectedPipette: SelectablePipettes, - isGantryEmpty: boolean + isGantryEmpty: boolean, + requiresFirmwareUpdate: boolean ): PipetteWizardStep[] => { switch (flowType) { case FLOWS.CALIBRATE: { @@ -32,7 +33,7 @@ export const getPipetteWizardSteps = ( } case FLOWS.ATTACH: { if (selectedPipette === SINGLE_MOUNT_PIPETTES) { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: mount, @@ -53,6 +54,9 @@ export const getPipetteWizardSteps = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) } else { // pipette needs to be detached before attached 96 channel if (!isGantryEmpty) { @@ -60,7 +64,7 @@ export const getPipetteWizardSteps = ( if (mount === LEFT) { detachMount = RIGHT } - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: detachMount, @@ -113,9 +117,14 @@ export const getPipetteWizardSteps = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter( + step => step.section !== SECTIONS.FIRMWARE_UPDATE + ) // gantry empty to attach 96 channel } else { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: mount, @@ -158,6 +167,11 @@ export const getPipetteWizardSteps = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter( + step => step.section !== SECTIONS.FIRMWARE_UPDATE + ) } } } diff --git a/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts b/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts index d63fe5d34ce..8a6cd324fb6 100644 --- a/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts +++ b/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts @@ -7,7 +7,8 @@ import type { PipetteWizardStep } from './types' export const getPipetteWizardStepsForProtocol = ( attachedPipettes: AttachedPipettesFromInstrumentsQuery, pipetteInfo: LoadedPipette[], - mount: Mount + mount: Mount, + requiresFirmwareUpdate: boolean ): PipetteWizardStep[] => { const requiredPipette = pipetteInfo.find(pipette => pipette.mount === mount) const nintySixChannelAttached = @@ -50,7 +51,7 @@ export const getPipetteWizardStepsForProtocol = ( ) { // 96-channel pipette attached and need to attach single mount pipette if (nintySixChannelAttached) { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: LEFT, @@ -99,9 +100,12 @@ export const getPipetteWizardStepsForProtocol = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) // Single mount pipette attached and need to attach new single mount pipette } else { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: mount, @@ -140,6 +144,9 @@ export const getPipetteWizardStepsForProtocol = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) } // Single mount pipette attached to both mounts and need to attach 96-channel pipette } else if ( @@ -147,7 +154,7 @@ export const getPipetteWizardStepsForProtocol = ( attachedPipettes[LEFT] != null && attachedPipettes[RIGHT] != null ) { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: LEFT, @@ -202,13 +209,16 @@ export const getPipetteWizardStepsForProtocol = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) // Single mount pipette attached to left mount and need to attach 96-channel pipette } else if ( requiredPipette.pipetteName === 'p1000_96' && attachedPipettes[LEFT] != null && attachedPipettes[RIGHT] == null ) { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: LEFT, @@ -257,13 +267,16 @@ export const getPipetteWizardStepsForProtocol = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) // Single mount pipette attached to right mount and need to attach 96-channel pipette } else if ( requiredPipette.pipetteName === 'p1000_96' && attachedPipettes[LEFT] == null && attachedPipettes[RIGHT] != null ) { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: RIGHT, @@ -312,11 +325,14 @@ export const getPipetteWizardStepsForProtocol = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) // if no pipette is attached to gantry } else { // Gantry empty and need to attach 96-channel pipette if (requiredPipette.pipetteName === 'p1000_96') { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: LEFT, @@ -359,9 +375,12 @@ export const getPipetteWizardStepsForProtocol = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) // Gantry empty and need to attach single mount pipette } else { - return [ + const ALL_STEPS = [ { section: SECTIONS.BEFORE_BEGINNING, mount: mount, @@ -394,6 +413,9 @@ export const getPipetteWizardStepsForProtocol = ( flowType: FLOWS.CALIBRATE, }, ] + return requiresFirmwareUpdate + ? ALL_STEPS + : ALL_STEPS.filter(step => step.section !== SECTIONS.FIRMWARE_UPDATE) } } } diff --git a/app/src/organisms/PipetteWizardFlows/index.tsx b/app/src/organisms/PipetteWizardFlows/index.tsx index 1a0dff58068..79ade3d8828 100644 --- a/app/src/organisms/PipetteWizardFlows/index.tsx +++ b/app/src/organisms/PipetteWizardFlows/index.tsx @@ -64,17 +64,26 @@ export const PipetteWizardFlows = ( const { t } = useTranslation('pipette_wizard_flows') const attachedPipettes = useAttachedPipettesFromInstrumentsQuery() + const attachedPipette = attachedPipettes.left ?? attachedPipettes.right + const requiresFirmwareUpdate = !attachedPipette?.ok const memoizedPipetteInfo = React.useMemo(() => props.pipetteInfo ?? null, []) const isGantryEmpty = attachedPipettes[LEFT] == null && attachedPipettes[RIGHT] == null const pipetteWizardSteps = React.useMemo( () => memoizedPipetteInfo == null - ? getPipetteWizardSteps(flowType, mount, selectedPipette, isGantryEmpty) + ? getPipetteWizardSteps( + flowType, + mount, + selectedPipette, + isGantryEmpty, + requiresFirmwareUpdate + ) : getPipetteWizardStepsForProtocol( attachedPipettes, memoizedPipetteInfo, - mount + mount, + requiresFirmwareUpdate ), [] ) diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index 96a5164ed19..16ceae4e002 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -6,6 +6,7 @@ import { useProtocolQuery, } from '@opentrons/react-api-client' import { getLabwareSetupItemGroups } from '../utils' +import { getProtocolUsesGripper } from '../../../organisms/ProtocolSetupInstruments/utils' import type { CompletedProtocolAnalysis, @@ -13,7 +14,7 @@ import type { PipetteName, } from '@opentrons/shared-data' import type { LabwareSetupItem } from '../utils' -import { getProtocolUsesGripper } from '../../../organisms/ProtocolSetupInstruments/utils' +import type { AttachedModule } from '@opentrons/api-client' interface ProtocolPipette { hardwareType: 'pipette' @@ -85,14 +86,24 @@ export const useRequiredProtocolHardware = ( ] : [] + const handleModuleConnectionCheckFor = ( + attachedModules: AttachedModule[], + model: ModuleModel + ): boolean => { + const ASSUME_ALWAYS_CONNECTED_MODULES = ['magneticBlockV1'] + + return !ASSUME_ALWAYS_CONNECTED_MODULES.includes(model) + ? attachedModules.some(m => m.moduleModel === model) + : true + } + const requiredModules: ProtocolModule[] = analysis.modules.map( ({ location, model }) => { return { hardwareType: 'module', moduleModel: model, slot: location.slotName as Slot, - // TODO: check module compatability using brent's changes when they're in edge - connected: attachedModules.some(m => m.moduleModel === model), + connected: handleModuleConnectionCheckFor(attachedModules, model), } } ) From b53815615b6653ff32cc4f3b78f1339a0d3a7c71 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Thu, 21 Sep 2023 13:23:15 -0400 Subject: [PATCH 12/79] fix(app): disable instrument card controls when run is active (#13601) fix RQA-1575, RQA-1515, RQA-1380 --- .../Devices/InstrumentsAndModules.tsx | 3 ++ .../PipetteCard/PipetteOverflowMenu.tsx | 5 ++ .../__tests__/PipetteCard.test.tsx | 11 +++++ .../__tests__/PipetteOverflowMenu.test.tsx | 1 + .../organisms/Devices/PipetteCard/index.tsx | 3 ++ .../RobotUpdateProgressModal.tsx | 48 +++++++++++-------- .../__tests__/GripperCard.test.tsx | 5 ++ app/src/organisms/GripperCard/index.tsx | 8 ++-- .../organisms/ModuleWizardFlows/Success.tsx | 17 +++++-- 9 files changed, 72 insertions(+), 29 deletions(-) diff --git a/app/src/organisms/Devices/InstrumentsAndModules.tsx b/app/src/organisms/Devices/InstrumentsAndModules.tsx index 86ad023f1ff..bb700732a26 100644 --- a/app/src/organisms/Devices/InstrumentsAndModules.tsx +++ b/app/src/organisms/Devices/InstrumentsAndModules.tsx @@ -213,6 +213,7 @@ export function InstrumentsAndModules({ pipetteIs96Channel={is96ChannelAttached} pipetteIsBad={badLeftPipette != null} updatePipette={() => setSubsystemToUpdate('pipette_left')} + isRunActive={currentRunId != null && !isRunTerminal} /> {isOT3 && ( )} {leftColumnModules.map((module, index) => ( @@ -263,6 +265,7 @@ export function InstrumentsAndModules({ pipetteIs96Channel={false} pipetteIsBad={badRightPipette != null} updatePipette={() => setSubsystemToUpdate('pipette_right')} + isRunActive={currentRunId != null && !isRunTerminal} /> )} {rightColumnModules.map((module, index) => ( diff --git a/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx b/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx index ee1399ea160..6107721a730 100644 --- a/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx +++ b/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx @@ -32,6 +32,7 @@ interface PipetteOverflowMenuProps { handleAboutSlideout: () => void handleSettingsSlideout: () => void isPipetteCalibrated: boolean + isRunActive: boolean } export const PipetteOverflowMenu = ( @@ -47,6 +48,7 @@ export const PipetteOverflowMenu = ( handleAboutSlideout, handleSettingsSlideout, isPipetteCalibrated, + isRunActive, } = props const pipetteName = @@ -72,6 +74,7 @@ export const PipetteOverflowMenu = ( handleChangePipette()} + disabled={isRunActive} data-testid={`pipetteOverflowMenu_attach_pipette_btn_${String( pipetteDisplayName )}_${mount}`} @@ -84,6 +87,7 @@ export const PipetteOverflowMenu = ( handleCalibrate()} + disabled={isRunActive} data-testid={`pipetteOverflowMenu_calibrate_offset_btn_${pipetteDisplayName}_${mount}`} > {t( @@ -96,6 +100,7 @@ export const PipetteOverflowMenu = ( handleChangePipette()} + disabled={isRunActive} data-testid={`pipetteOverflowMenu_detach_pipette_btn_${String( pipetteDisplayName )}_${mount}`} diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx index 4ec2b1b36d2..2e68851c5c5 100644 --- a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx +++ b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx @@ -88,6 +88,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } when(mockUseIsOT3).calledWith(mockRobotName).mockReturnValue(false) when(mockAboutPipettesSlideout).mockReturnValue( @@ -131,6 +132,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('left Mount') @@ -146,6 +148,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } const { getByText, getByRole } = render(props) getByText('Both Mounts') @@ -166,6 +169,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('right Mount') @@ -180,6 +184,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('right Mount') @@ -194,6 +199,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('left Mount') @@ -209,6 +215,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } const { queryByText } = render(props) expect(queryByText('Calibrate now')).toBeNull() @@ -223,6 +230,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('Calibrate now') @@ -236,6 +244,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: false, updatePipette: jest.fn(), + isRunActive: false, } const { getByRole, getByText, queryByText } = render(props) @@ -258,6 +267,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: true, updatePipette: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('Right mount') @@ -278,6 +288,7 @@ describe('PipetteCard', () => { isPipetteCalibrated: false, pipetteIsBad: true, updatePipette: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('Right mount') diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx index 00d6a6d2355..7a534b4956e 100644 --- a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx @@ -45,6 +45,7 @@ describe('PipetteOverflowMenu', () => { handleAboutSlideout: jest.fn(), handleSettingsSlideout: jest.fn(), isPipetteCalibrated: false, + isRunActive: false, } }) afterEach(() => { diff --git a/app/src/organisms/Devices/PipetteCard/index.tsx b/app/src/organisms/Devices/PipetteCard/index.tsx index 6cb41f0560b..ff44bcf3589 100644 --- a/app/src/organisms/Devices/PipetteCard/index.tsx +++ b/app/src/organisms/Devices/PipetteCard/index.tsx @@ -62,6 +62,7 @@ interface PipetteCardProps { pipetteIsBad: boolean updatePipette: () => void pipetteId?: AttachedPipette['id'] | null + isRunActive: boolean } const BANNER_LINK_STYLE = css` text-decoration: underline; @@ -92,6 +93,7 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { pipetteIs96Channel, pipetteIsBad, updatePipette, + isRunActive, } = props const { menuOverlay, @@ -351,6 +353,7 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { handleCalibrate={handleCalibrate} isPipetteCalibrated={isPipetteCalibrated} pipetteSettings={settings} + isRunActive={isRunActive} /> {menuOverlay} diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx index a051f2af5d5..31ca9616b58 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx @@ -42,11 +42,16 @@ function SuccessOrError({ errorMessage }: SuccessOrErrorProps): JSX.Element { let renderedImg: JSX.Element if (!errorMessage) renderedImg = ( - {IMAGE_ALT} + {IMAGE_ALT} ) else renderedImg = ( - + ) return ( @@ -104,6 +109,7 @@ function RobotUpdateProgressFooter({ onClick={installUpdate} marginRight={SPACING.spacing8} css={FOOTER_BUTTON_STYLE} + border="none" > {t('try_again')} @@ -218,26 +224,26 @@ export function RobotUpdateProgressModal({ ) : null } > - - {completedUpdating ? ( + {completedUpdating ? ( + - ) : ( - <> - {modalBodyText} - - - {t('do_not_turn_off')} - - - )} - + + ) : ( + + {modalBodyText} + + + {t('do_not_turn_off')} + + + )} ) } diff --git a/app/src/organisms/GripperCard/__tests__/GripperCard.test.tsx b/app/src/organisms/GripperCard/__tests__/GripperCard.test.tsx index 76ab0d39601..2edf2484a56 100644 --- a/app/src/organisms/GripperCard/__tests__/GripperCard.test.tsx +++ b/app/src/organisms/GripperCard/__tests__/GripperCard.test.tsx @@ -46,6 +46,7 @@ describe('GripperCard', () => { } as GripperData, isCalibrated: true, setSubsystemToUpdate: jest.fn(), + isRunActive: false, } mockGripperWizardFlows.mockReturnValue(<>wizard flow launched) mockAboutGripperSlideout.mockReturnValue(<>about gripper) @@ -87,6 +88,7 @@ describe('GripperCard', () => { } as GripperData, isCalibrated: false, setSubsystemToUpdate: jest.fn(), + isRunActive: false, } const { getByText } = render(props) @@ -127,6 +129,7 @@ describe('GripperCard', () => { attachedGripper: null, isCalibrated: false, setSubsystemToUpdate: jest.fn(), + isRunActive: false, } const { getByText, getByRole } = render(props) const overflowButton = getByRole('button', { @@ -144,6 +147,7 @@ describe('GripperCard', () => { } as any, isCalibrated: false, setSubsystemToUpdate: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('Extension mount') @@ -162,6 +166,7 @@ describe('GripperCard', () => { } as any, isCalibrated: true, setSubsystemToUpdate: jest.fn(), + isRunActive: false, } const { getByText } = render(props) getByText('Extension mount') diff --git a/app/src/organisms/GripperCard/index.tsx b/app/src/organisms/GripperCard/index.tsx index 0c900e88576..084e7d7198d 100644 --- a/app/src/organisms/GripperCard/index.tsx +++ b/app/src/organisms/GripperCard/index.tsx @@ -17,6 +17,7 @@ interface GripperCardProps { attachedGripper: GripperData | BadGripper | null isCalibrated: boolean setSubsystemToUpdate: (subsystem: Subsystem | null) => void + isRunActive: boolean } const BANNER_LINK_CSS = css` text-decoration: underline; @@ -40,6 +41,7 @@ export function GripperCard({ attachedGripper, isCalibrated, setSubsystemToUpdate, + isRunActive, }: GripperCardProps): JSX.Element { const { t, i18n } = useTranslation(['device_details', 'shared']) const [ @@ -73,7 +75,7 @@ export function GripperCard({ ? [ { label: t('attach_gripper'), - disabled: attachedGripper != null, + disabled: attachedGripper != null || isRunActive, onClick: handleAttach, }, ] @@ -83,12 +85,12 @@ export function GripperCard({ attachedGripper.data.calibratedOffset?.last_modified != null ? t('recalibrate_gripper') : t('calibrate_gripper'), - disabled: attachedGripper == null, + disabled: attachedGripper == null || isRunActive, onClick: handleCalibrate, }, { label: t('detach_gripper'), - disabled: attachedGripper == null, + disabled: attachedGripper == null || isRunActive, onClick: handleDetach, }, { diff --git a/app/src/organisms/ModuleWizardFlows/Success.tsx b/app/src/organisms/ModuleWizardFlows/Success.tsx index 2e0847f02f2..16b8273214f 100644 --- a/app/src/organisms/ModuleWizardFlows/Success.tsx +++ b/app/src/organisms/ModuleWizardFlows/Success.tsx @@ -4,11 +4,12 @@ import { css } from 'styled-components' import { getModuleDisplayName } from '@opentrons/shared-data' import { COLORS, + JUSTIFY_FLEX_END, PrimaryButton, RESPONSIVENESS, TYPOGRAPHY, } from '@opentrons/components' - +import { SmallButton } from '../../atoms/buttons' import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' import type { ModuleCalibrationWizardStepProps } from './types' @@ -24,13 +25,20 @@ export const BODY_STYLE = css` export const Success = ( props: ModuleCalibrationWizardStepProps ): JSX.Element | null => { - const { proceed, attachedModule, isRobotMoving } = props + const { proceed, attachedModule, isRobotMoving, isOnDevice } = props const { t } = useTranslation('module_wizard_flows') const moduleDisplayName = getModuleDisplayName(attachedModule.moduleModel) const handleOnClick = (): void => { proceed() } + const button = isOnDevice ? ( + + ) : ( + + {t('exit')} + + ) return ( - - {t('exit')} - + {button} ) } From 24eb3c1f135a2736a65f478620180a9026508d9b Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 28 Sep 2023 13:43:05 -0400 Subject: [PATCH 13/79] fix(api): New protocols print a JSON "run log" from opentrons_execute and opentrons.execute.execute() (#13629) --- api/src/opentrons/execute.py | 51 +++++++-- .../protocol_engine/command_monitor.py | 65 ++++++++++++ .../protocol_engine/protocol_engine.py | 32 ++++++ .../protocol_engine/state/change_notifier.py | 12 +++ .../protocol_engine/state/commands.py | 6 +- .../opentrons/protocol_engine/state/state.py | 15 ++- .../protocol_runner/legacy_context_plugin.py | 11 +- api/src/opentrons/util/broker.py | 51 +++++++-- .../state/test_change_notifier.py | 21 +++- .../state/test_command_monitor.py | 92 ++++++++++++++++ .../state/test_command_view.py | 9 ++ .../test_legacy_context_plugin.py | 60 +++++++---- api/tests/opentrons/test_execute.py | 100 +++++++++++++----- api/tests/opentrons/util/test_broker.py | 27 ++++- 14 files changed, 473 insertions(+), 79 deletions(-) create mode 100644 api/src/opentrons/protocol_engine/command_monitor.py create mode 100644 api/tests/opentrons/protocol_engine/state/test_command_monitor.py diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index 38a8db44734..709cd2c6c69 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -41,7 +41,6 @@ ThreadManagedHardware, ThreadManager, ) -from opentrons.protocol_engine.types import PostRunHardwareState from opentrons.protocols import parse from opentrons.protocols.api_support.deck_type import ( @@ -63,9 +62,11 @@ DeckType, EngineStatus, ErrorOccurrence as ProtocolEngineErrorOccurrence, + command_monitor as pe_command_monitor, create_protocol_engine, create_protocol_engine_in_thread, ) +from opentrons.protocol_engine.types import PostRunHardwareState from opentrons.protocol_reader import ProtocolReader, ProtocolSource @@ -407,13 +408,6 @@ def execute( # noqa: C901 else: # TODO(mm, 2023-07-06): Once these NotImplementedErrors are resolved, consider removing # the enclosing if-else block and running everything through _run_file_pe() for simplicity. - if emit_runlog: - raise NotImplementedError( - f"Printing the run log is not currently supported for Python protocols" - f" with apiLevel {ENGINE_CORE_API_VERSION} or newer." - f" Pass --no-print-runlog to opentrons_execute" - f" or emit_runlog=None to opentrons.execute.execute()." - ) if custom_data_paths: raise NotImplementedError( f"The custom_data_paths argument is not currently supported for Python protocols" @@ -425,6 +419,7 @@ def execute( # noqa: C901 protocol_name=protocol_name, extra_labware=extra_labware, hardware_api=_get_global_hardware_controller(_get_robot_type()).wrapped(), + emit_runlog=emit_runlog, ) @@ -642,9 +637,14 @@ def _run_file_pe( protocol_name: str, extra_labware: Dict[str, FoundLabware], hardware_api: HardwareControlAPI, + emit_runlog: Optional[_EmitRunlogCallable], ) -> None: """Run a protocol file with Protocol Engine.""" + def send_command_to_emit_runlog(event: pe_command_monitor.Event) -> None: + if emit_runlog is not None: + emit_runlog(_adapt_command(event)) + async def run(protocol_source: ProtocolSource) -> None: protocol_engine = await create_protocol_engine( hardware_api=hardware_api, @@ -657,9 +657,12 @@ async def run(protocol_source: ProtocolSource) -> None: hardware_api=hardware_api, ) - # TODO(mm, 2023-06-30): This will home and drop tips at the end, which is not how - # things have historically behaved with PAPIv2.13 and older or JSONv5 and older. - result = await protocol_runner.run(protocol_source) + with pe_command_monitor.monitor_commands( + protocol_engine, callback=send_command_to_emit_runlog + ): + # TODO(mm, 2023-06-30): This will home and drop tips at the end, which is not how + # things have historically behaved with PAPIv2.13 and older or JSONv5 and older. + result = await protocol_runner.run(protocol_source) if result.state_summary.status != EngineStatus.SUCCEEDED: raise _ProtocolEngineExecuteError(result.state_summary.errors) @@ -732,6 +735,32 @@ def _adapt_protocol_source( yield protocol_source +def _adapt_command(event: pe_command_monitor.Event) -> command_types.CommandMessage: + """Convert a Protocol Engine command event to an old-school command_types.CommandMesage.""" + before_or_after: command_types.MessageSequenceId = ( + "before" if isinstance(event, pe_command_monitor.RunningEvent) else "after" + ) + + message: command_types.CommentMessage = { + # TODO(mm, 2023-09-26): If we can without breaking the public API, remove the requirement + # to supply a "name" here. If we can't do that, consider adding a special name value + # so we don't have to lie and call every command a comment. + "name": "command.COMMENT", + "id": event.command.id, + "$": before_or_after, + # TODO(mm, 2023-09-26): Convert this machine-readable JSON into a human-readable message + # to match behavior from before Protocol Engine. + # https://opentrons.atlassian.net/browse/RSS-320 + "payload": {"text": event.command.json()}, + # As far as I know, "error" is not part of the public-facing API, so it doesn't matter + # what we put here. Leaving it as `None` to avoid difficulties in converting between + # the Protocol Engine `ErrorOccurrence` model and the regular Python `Exception` type + # that this field expects. + "error": None, + } + return message + + def _get_global_hardware_controller(robot_type: RobotType) -> ThreadManagedHardware: # Build a hardware controller in a worker thread, which is necessary # because ipython runs its notebook in asyncio but the notebook diff --git a/api/src/opentrons/protocol_engine/command_monitor.py b/api/src/opentrons/protocol_engine/command_monitor.py new file mode 100644 index 00000000000..9f2985f59b0 --- /dev/null +++ b/api/src/opentrons/protocol_engine/command_monitor.py @@ -0,0 +1,65 @@ +"""Monitor the execution of commands in a `ProtocolEngine`.""" + + +from dataclasses import dataclass +import typing +import contextlib + + +from opentrons.protocol_engine import Command, ProtocolEngine + + +@dataclass +class RunningEvent: + """Emitted when a command starts running.""" + + command: Command + + +@dataclass +class NoLongerRunningEvent: + """Emitted when a command stops running--either because it succeeded, or failed.""" + + command: Command + + +Event = typing.Union[RunningEvent, NoLongerRunningEvent] +Callback = typing.Callable[[Event], None] + + +@contextlib.contextmanager +def monitor_commands( + protocol_engine: ProtocolEngine, + callback: Callback, +) -> typing.Generator[None, None, None]: + """Monitor the execution of commands in `protocol_engine`. + + While this context manager is open, `callback` will be called any time `protocol_engine` + starts or stops a command. + """ + # Subscribe to all state updates in protocol_engine. + # On every update, diff the new state against the last state and see if the currently + # running command has changed. If it has, emit the appropriate events. + + last_running_id: typing.Optional[str] = None + + def handle_state_update(_message_from_broker: None) -> None: + nonlocal last_running_id + + running_id = protocol_engine.state_view.commands.get_running() + if running_id != last_running_id: + if last_running_id is not None: + callback( + NoLongerRunningEvent( + protocol_engine.state_view.commands.get(last_running_id) + ) + ) + + if running_id is not None: + callback( + RunningEvent(protocol_engine.state_view.commands.get(running_id)) + ) + last_running_id = running_id + + with protocol_engine.state_update_broker.subscribed(handle_state_update): + yield diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 207ebddd9d8..9ad7cb3a27c 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -12,6 +12,8 @@ EnumeratedError, ) +from opentrons.util.broker import ReadOnlyBroker + from .errors import ProtocolCommandFailedError, ErrorOccurrence from .errors.exceptions import EStopActivatedError from . import commands, slot_standardization @@ -129,6 +131,36 @@ def state_view(self) -> StateView: """Get an interface to retrieve calculated state values.""" return self._state_store + @property + def state_update_broker(self) -> ReadOnlyBroker[None]: + """Return a broker that you can use to get notified of all state updates. + + For example, you can use this to do something any time a new command starts running. + + `ProtocolEngine` will publish a message to this broker (with the placeholder value `None`) + any time its state updates. Then, when you receive that message, you can get the latest + state through `state_view` and inspect it to see whether something happened that you care + about. + + Warning: + Use this mechanism sparingly, because it has several footguns: + + * Your callbacks will run synchronously, on every state update. + If they take a long time, they will harm analysis and run speed. + + * Your callbacks will run in the thread and asyncio event loop that own this + `ProtocolEngine`. (See the concurrency notes in the `ProtocolEngine` docstring.) + If your callbacks interact with things in other threads or event loops, + take appropriate precautions to keep them concurrency-safe. + + * Currently, if your callback raises an exception, it will propagate into + `ProtocolEngine` and be treated like any other internal error. This will probably + stop the run. If you expect your code to raise exceptions and don't want + that to happen, consider catching and logging them at the top level of your callback, + before they propagate into `ProtocolEngine`. + """ + return self._state_store.update_broker + def add_plugin(self, plugin: AbstractPlugin) -> None: """Add a plugin to the engine to customize behavior.""" self._plugin_starter.start(plugin) diff --git a/api/src/opentrons/protocol_engine/state/change_notifier.py b/api/src/opentrons/protocol_engine/state/change_notifier.py index 3c72f277913..629cb89f368 100644 --- a/api/src/opentrons/protocol_engine/state/change_notifier.py +++ b/api/src/opentrons/protocol_engine/state/change_notifier.py @@ -1,6 +1,8 @@ """Simple state change notification interface.""" import asyncio +from opentrons.util.broker import Broker, ReadOnlyBroker + class ChangeNotifier: """An interface tto emit or subscribe to state change notifications.""" @@ -8,12 +10,22 @@ class ChangeNotifier: def __init__(self) -> None: """Initialize the ChangeNotifier with an internal Event.""" self._event = asyncio.Event() + self._broker = Broker[None]() def notify(self) -> None: """Notify all `wait`'ers that the state has changed.""" self._event.set() + self._broker.publish(None) async def wait(self) -> None: """Wait until the next state change notification.""" self._event.clear() await self._event.wait() + + @property + def broker(self) -> ReadOnlyBroker[None]: + """Return a broker that you can use to get notified of all changes. + + This is an alternative interface to `wait()`. + """ + return self._broker diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index b4301c22920..ae0ba7898cb 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -507,13 +507,17 @@ def get_error(self) -> Optional[ErrorOccurrence]: else: return run_error or finish_error + def get_running(self) -> Optional[str]: + """Return the ID of the command that's currently running, if any.""" + return self._state.running_command_id + def get_current(self) -> Optional[CurrentCommand]: """Return the "current" command, if any. The "current" command is the command that is currently executing, or the most recent command to have completed. """ - if self._state.running_command_id: + if self._state.running_command_id is not None: entry = self._state.commands_by_id[self._state.running_command_id] return CurrentCommand( command_id=entry.command.id, diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index e2ffc888e85..761056bdc87 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -4,10 +4,12 @@ from dataclasses import dataclass from functools import partial from typing import Any, Callable, Dict, List, Optional, Sequence, TypeVar -from opentrons.protocol_engine.types import ModuleOffsetVector from opentrons_shared_data.deck.dev_types import DeckDefinitionV3 +from opentrons.protocol_engine.types import ModuleOffsetVector +from opentrons.util.broker import ReadOnlyBroker + from ..resources import DeckFixedLabware from ..actions import Action, ActionHandler from .abstract_store import HasState, HandlesActions @@ -238,6 +240,17 @@ async def wait_for( return is_done + # We return ReadOnlyBroker[None] instead of ReadOnlyBroker[StateView] in order to avoid + # confusion with state mutability. If a caller needs to know the new state, they can + # retrieve it explicitly with `ProtocolEngine.state_view`. + @property + def update_broker(self) -> ReadOnlyBroker[None]: + """Return a broker that you can use to get notified of all state updates. + + This is an alternative interface to `wait_for()`. + """ + return self._change_notifier.broker + def _get_next_state(self) -> State: """Get a new instance of the state value object.""" return State( diff --git a/api/src/opentrons/protocol_runner/legacy_context_plugin.py b/api/src/opentrons/protocol_runner/legacy_context_plugin.py index 24a2cda49f6..41ba0c62268 100644 --- a/api/src/opentrons/protocol_runner/legacy_context_plugin.py +++ b/api/src/opentrons/protocol_runner/legacy_context_plugin.py @@ -8,7 +8,7 @@ from opentrons.commands.types import CommandMessage as LegacyCommand from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_engine import AbstractPlugin, actions as pe_actions -from opentrons.util.broker import Broker +from opentrons.util.broker import ReadOnlyBroker from .legacy_wrappers import LegacyLoadInfo from .legacy_command_mapper import LegacyCommandMapper @@ -37,7 +37,7 @@ class LegacyContextPlugin(AbstractPlugin): def __init__( self, broker: LegacyBroker, - equipment_broker: Broker[LegacyLoadInfo], + equipment_broker: ReadOnlyBroker[LegacyLoadInfo], legacy_command_mapper: Optional[LegacyCommandMapper] = None, ) -> None: """Initialize the plugin with its dependencies.""" @@ -78,10 +78,11 @@ def setup(self) -> None: ) exit_stack.callback(command_broker_unsubscribe) - equipment_broker_unsubscribe = self._equipment_broker.subscribe( - callback=self._handle_equipment_loaded + exit_stack.enter_context( + self._equipment_broker.subscribed( + callback=self._handle_equipment_loaded + ) ) - exit_stack.callback(equipment_broker_unsubscribe) # All subscriptions succeeded. # Save the exit stack so our teardown method can use it later diff --git a/api/src/opentrons/util/broker.py b/api/src/opentrons/util/broker.py index 5459735b1b8..bbdd717ab11 100644 --- a/api/src/opentrons/util/broker.py +++ b/api/src/opentrons/util/broker.py @@ -1,26 +1,63 @@ """A simple pub/sub message broker.""" -from typing import Callable, Generic, Set, TypeVar +from abc import ABC, abstractmethod +from contextlib import contextmanager +from typing import Callable, ContextManager, Generator, Generic, Set, TypeVar _MessageT = TypeVar("_MessageT") +_CallbackT = Callable[[_MessageT], None] -class Broker(Generic[_MessageT]): +class ReadOnlyBroker(ABC, Generic[_MessageT]): + """The read-only subset of `Broker`. + + Useful for typing if you want people to be able to subscribe to your `Broker`, + but don't want them to be able to publish their own messages to it. + """ + + @abstractmethod + def subscribed(self, callback: _CallbackT[_MessageT]) -> ContextManager[None]: + """See `Broker.subscribed()`.""" # noqa: D402 + pass + + @abstractmethod + def subscribe(self, callback: _CallbackT[_MessageT]) -> Callable[[], None]: + """See `Broker.subscribe()`.""" # noqa: D402 + pass + + +class Broker(Generic[_MessageT], ReadOnlyBroker[_MessageT]): """A simple pub/sub message broker. Subscribers can listen to events. Publishers can push events to all subscribers. """ def __init__(self) -> None: - self._callbacks: Set[Callable[[_MessageT], None]] = set() + self._callbacks: Set[_CallbackT[_MessageT]] = set() + + @contextmanager + def subscribed( + self, callback: _CallbackT[_MessageT] + ) -> Generator[None, None, None]: + """Register a callback to be called on each message. + + The callback is subscribed when this context manager is entered, + and unsubscribed when it's exited. + + You must not subscribe the same callback again unless you first usubscribe it. + """ + unsubscribe = self.subscribe(callback) + try: + yield + finally: + unsubscribe() - def subscribe(self, callback: Callable[[_MessageT], None]) -> Callable[[], None]: - """Register ``callback`` to be called by a subsequent `publish`. + def subscribe(self, callback: _CallbackT[_MessageT]) -> Callable[[], None]: + """Register a callback to be called on each message. - You must not subscribe the same callback again - unless you first unsubscribe it. + You must not subscribe the same callback again unless you first unsubscribe it. Returns: A function that you can call to unsubscribe ``callback``. diff --git a/api/tests/opentrons/protocol_engine/state/test_change_notifier.py b/api/tests/opentrons/protocol_engine/state/test_change_notifier.py index 8939c17df0d..ec62362d6da 100644 --- a/api/tests/opentrons/protocol_engine/state/test_change_notifier.py +++ b/api/tests/opentrons/protocol_engine/state/test_change_notifier.py @@ -19,8 +19,8 @@ async def test_single_subscriber() -> None: await result -@pytest.mark.parametrize("count", range(10)) -async def test_multiple_subscribers(count: int) -> None: +@pytest.mark.parametrize("_test_repetition", range(10)) +async def test_multiple_subscribers(_test_repetition: int) -> None: """Test that multiple subscribers can wait for a notification. This test checks that the subscribers are awoken in the order they @@ -54,3 +54,20 @@ async def _do_task_3() -> None: await asyncio.gather(task_1, task_2, task_3) assert results == [1, 2, 3] + + +async def test_broker() -> None: + """Test that notifications are available synchronously through `ChangeNotifier.broker`.""" + notify_count = 5 + + subject = ChangeNotifier() + received = 0 + + def callback(_message_from_broker: None) -> None: + nonlocal received + received += 1 + + with subject.broker.subscribed(callback): + for notify_number in range(notify_count): + subject.notify() + assert received == notify_number + 1 diff --git a/api/tests/opentrons/protocol_engine/state/test_command_monitor.py b/api/tests/opentrons/protocol_engine/state/test_command_monitor.py new file mode 100644 index 00000000000..dec820a97f6 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_command_monitor.py @@ -0,0 +1,92 @@ +"""Unit tests for `opentrons.protocol_engine.command_monitor`.""" + + +from datetime import datetime +from typing import List + +from decoy import Decoy + +from opentrons.protocol_engine import CommandStatus, ProtocolEngine, commands +from opentrons.protocol_engine.command_monitor import ( + Event, + NoLongerRunningEvent, + RunningEvent, + monitor_commands as subject, +) +from opentrons.util.broker import Broker + + +def _make_dummy_command(id: str, completed: bool) -> commands.Command: + if completed: + return commands.Comment( + id=id, + key=id, + status=CommandStatus.SUCCEEDED, + createdAt=datetime(2023, 9, 26), + params=commands.CommentParams(message=""), + result=None, + ) + else: + return commands.Comment( + id=id, + key=id, + status=CommandStatus.RUNNING, + createdAt=datetime(2023, 9, 26), + completedAt=datetime(2023, 9, 26), + params=commands.CommentParams(message=""), + result=commands.CommentResult(), + ) + + +def test_monitor_commands(decoy: Decoy) -> None: + """Test that it translates state updates into command running/no-longer-running events.""" + mock_protocol_engine = decoy.mock(cls=ProtocolEngine) + mock_command_view = mock_protocol_engine.state_view.commands + state_update_broker = Broker[None]() + decoy.when(mock_protocol_engine.state_update_broker).then_return( + state_update_broker + ) + + command_1_running = _make_dummy_command(id="command-1", completed=False) + command_1_completed = _make_dummy_command(id="command-1", completed=True) + command_2_running = _make_dummy_command(id="command-2", completed=False) + command_2_completed = _make_dummy_command(id="command-2", completed=True) + + received_events: List[Event] = [] + + def callback(event: Event) -> None: + received_events.append(event) + + with subject(mock_protocol_engine, callback): + # Feed the subject these states, in sequence: + # 1. No command running + # 2. "command-1" running + # 3. "command-2" running + # 4. No command running + # Between each state, notify the subject by publishing a message to the broker that it's + # subscribed to. + + decoy.when(mock_command_view.get_running()).then_return(None) + state_update_broker.publish(message=None) + + decoy.when(mock_command_view.get_running()).then_return("command-1") + decoy.when(mock_command_view.get("command-1")).then_return(command_1_running) + state_update_broker.publish(message=None) + + decoy.when(mock_command_view.get_running()).then_return("command-2") + decoy.when(mock_command_view.get("command-1")).then_return(command_1_completed) + decoy.when(mock_command_view.get("command-2")).then_return(command_2_running) + state_update_broker.publish(message=None) + + decoy.when(mock_command_view.get_running()).then_return(None) + decoy.when(mock_command_view.get("command-2")).then_return(command_2_completed) + state_update_broker.publish(message=None) + + # Make sure the callback converted the sequence of state updates into the expected sequence + # of events. + assert received_events == [ + RunningEvent(command_1_running), + NoLongerRunningEvent(command_1_completed), + RunningEvent(command_2_running), + NoLongerRunningEvent(command_2_completed), + ] diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view.py index b9cc6835ce3..d4f77db8dbe 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view.py @@ -660,6 +660,15 @@ def test_get_okay_to_clear(subject: CommandView, expected_is_okay: bool) -> None assert subject.get_is_okay_to_clear() is expected_is_okay +def test_get_running() -> None: + """It should return the command that's currently running.""" + subject = get_command_view(running_command_id=None) + assert subject.get_running() is None + + subject = get_command_view(running_command_id="command-id") + assert subject.get_running() == "command-id" + + def test_get_current() -> None: """It should return the "current" command.""" subject = get_command_view( diff --git a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py index ebd7c01b995..ac9a46112ff 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py @@ -12,7 +12,7 @@ commands as pe_commands, ) from opentrons.legacy_broker import LegacyBroker -from opentrons.util.broker import Broker +from opentrons.util.broker import ReadOnlyBroker from opentrons.protocol_runner.legacy_command_mapper import LegacyCommandMapper from opentrons.protocol_runner.legacy_context_plugin import LegacyContextPlugin @@ -30,14 +30,14 @@ @pytest.fixture def mock_legacy_broker(decoy: Decoy) -> LegacyBroker: - """Get a mocked out LegacyProtocolContext dependency.""" + """Get a mocked out `broker: LegacyBroker` dependency.""" return decoy.mock(cls=LegacyBroker) @pytest.fixture -def mock_broker(decoy: Decoy) -> Broker[LegacyLoadInfo]: - """Get a mocked out LegacyProtocolContext dependency.""" - return decoy.mock(cls=Broker[LegacyLoadInfo]) +def mock_equipment_broker(decoy: Decoy) -> ReadOnlyBroker[LegacyLoadInfo]: + """Get a mocked out `equipment_broker: Broker` dependency.""" + return decoy.mock(cls=ReadOnlyBroker[LegacyLoadInfo]) @pytest.fixture @@ -61,7 +61,7 @@ def mock_action_dispatcher(decoy: Decoy) -> pe_actions.ActionDispatcher: @pytest.fixture def subject( mock_legacy_broker: LegacyBroker, - mock_broker: Broker[LegacyLoadInfo], + mock_equipment_broker: ReadOnlyBroker[LegacyLoadInfo], mock_legacy_command_mapper: LegacyCommandMapper, mock_state_view: StateView, mock_action_dispatcher: pe_actions.ActionDispatcher, @@ -69,42 +69,58 @@ def subject( """Get a configured LegacyContextPlugin with its dependencies mocked out.""" plugin = LegacyContextPlugin( broker=mock_legacy_broker, - equipment_broker=mock_broker, + equipment_broker=mock_equipment_broker, legacy_command_mapper=mock_legacy_command_mapper, ) plugin._configure(state=mock_state_view, action_dispatcher=mock_action_dispatcher) return plugin +class _ContextManager: + """A placeholder in the shape of a context manager, to pass as a template to `decoy.mock()`.""" + + def __enter__(self) -> None: + raise NotImplementedError() + + def __exit__(self, type: object, value: object, traceback: object) -> None: + raise NotImplementedError() + + async def test_broker_subscribe_unsubscribe( decoy: Decoy, mock_legacy_broker: LegacyBroker, - mock_broker: Broker[LegacyLoadInfo], + mock_equipment_broker: ReadOnlyBroker[LegacyLoadInfo], subject: LegacyContextPlugin, ) -> None: """It should subscribe to the brokers on setup and unsubscribe on teardown.""" command_broker_unsubscribe: Callable[[], None] = decoy.mock() - equipment_broker_unsubscribe: Callable[[], None] = decoy.mock() + equipment_broker_subscription_context = decoy.mock(cls=_ContextManager) decoy.when( mock_legacy_broker.subscribe(topic="command", handler=matchers.Anything()) ).then_return(command_broker_unsubscribe) - decoy.when(mock_broker.subscribe(callback=matchers.Anything())).then_return( - equipment_broker_unsubscribe - ) + decoy.when( + mock_equipment_broker.subscribed(callback=matchers.Anything()) + ).then_return(equipment_broker_subscription_context) subject.setup() await subject.teardown() decoy.verify(command_broker_unsubscribe()) - decoy.verify(equipment_broker_unsubscribe()) + + decoy.verify( + [ + equipment_broker_subscription_context.__enter__(), # type: ignore[func-returns-value] + equipment_broker_subscription_context.__exit__(None, None, None), # type: ignore[func-returns-value] + ] + ) async def test_command_broker_messages( decoy: Decoy, mock_legacy_broker: LegacyBroker, - mock_broker: Broker[LegacyLoadInfo], + mock_equipment_broker: ReadOnlyBroker[LegacyLoadInfo], mock_legacy_command_mapper: LegacyCommandMapper, mock_action_dispatcher: pe_actions.ActionDispatcher, subject: LegacyContextPlugin, @@ -117,9 +133,9 @@ async def test_command_broker_messages( decoy.when( mock_legacy_broker.subscribe(topic="command", handler=command_handler_captor) ).then_return(decoy.mock()) - decoy.when(mock_broker.subscribe(callback=matchers.Anything())).then_return( - decoy.mock() - ) + decoy.when( + mock_equipment_broker.subscribed(callback=matchers.Anything()) + ).then_enter_with(None) subject.setup() @@ -158,7 +174,7 @@ async def test_command_broker_messages( async def test_equipment_broker_messages( decoy: Decoy, mock_legacy_broker: LegacyBroker, - mock_broker: Broker[LegacyLoadInfo], + mock_equipment_broker: ReadOnlyBroker[LegacyLoadInfo], mock_legacy_command_mapper: LegacyCommandMapper, mock_action_dispatcher: pe_actions.ActionDispatcher, subject: LegacyContextPlugin, @@ -166,15 +182,13 @@ async def test_equipment_broker_messages( ) -> None: """It should dispatch commands from equipment broker messages.""" # Capture the function that the plugin sets up as its labware load callback. - # Also, ensure that all subscribe calls return an actual unsubscribe callable - # (instead of Decoy's default `None`) so subject.teardown() works. labware_handler_captor = matchers.Captor() decoy.when( mock_legacy_broker.subscribe(topic="command", handler=matchers.Anything()) ).then_return(decoy.mock()) - decoy.when(mock_broker.subscribe(callback=labware_handler_captor)).then_return( - decoy.mock() - ) + decoy.when( + mock_equipment_broker.subscribed(callback=labware_handler_captor) + ).then_enter_with(None) subject.setup() diff --git a/api/tests/opentrons/test_execute.py b/api/tests/opentrons/test_execute.py index e986fc1ed7c..d233914af24 100644 --- a/api/tests/opentrons/test_execute.py +++ b/api/tests/opentrons/test_execute.py @@ -58,24 +58,10 @@ async def dummy_delay(self: Any, duration_s: float) -> None: return gai_mock -@pytest.mark.parametrize( - ("protocol_file", "expect_run_log"), - [ - ("testosaur_v2.py", True), - ("testosaur_v2_14.py", False), - # FIXME(mm, 2023-07-20): Support printing the run log when executing new protocols. - # Then, remove this expect_run_log parametrization (it should always be True). - pytest.param( - "testosaur_v2_14.py", - True, - marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), - ), - ], -) +@pytest.mark.parametrize("protocol_file", ["testosaur_v2.py"]) def test_execute_function_apiv2( protocol: Protocol, protocol_file: str, - expect_run_log: bool, virtual_smoothie_env: None, mock_get_attached_instr: mock.AsyncMock, ) -> None: @@ -109,21 +95,79 @@ def emit_runlog(entry: Any) -> None: nonlocal entries entries.append(entry) - execute.execute( - protocol.filelike, - protocol.filename, - emit_runlog=(emit_runlog if expect_run_log else None), + execute.execute(protocol.filelike, protocol.filename, emit_runlog=emit_runlog) + + assert [item["payload"]["text"] for item in entries if item["$"] == "before"] == [ + "Picking up tip from A1 of Opentrons 96 Tip Rack 1000 µL on 1", + "Aspirating 100.0 uL from A1 of Corning 96 Well Plate 360 µL Flat on 2 at 500.0 uL/sec", + "Dispensing 100.0 uL into B1 of Corning 96 Well Plate 360 µL Flat on 2 at 1000.0 uL/sec", + "Dropping tip into H12 of Opentrons 96 Tip Rack 1000 µL on 1", + ] + + +# TODO(mm, 2023-09-26): Merge this with the above test_execute_apiv2_14() function when +# we resolve https://opentrons.atlassian.net/browse/RSS-320 and PAPIv≥2.14 protocols emit +# human-readable run log text. +@pytest.mark.parametrize("protocol_file", ["testosaur_v2_14.py"]) +def test_execute_function_apiv2_14( + protocol: Protocol, + protocol_file: str, + virtual_smoothie_env: None, + mock_get_attached_instr: mock.AsyncMock, +) -> None: + """Test `execute()` with a Python file.""" + converted_model_v15 = pipette_load_name.convert_pipette_model( + cast(PipetteModel, "p10_single_v1.5") + ) + converted_model_v1 = pipette_load_name.convert_pipette_model( + cast(PipetteModel, "p1000_single_v1") ) - if expect_run_log: - assert [ - item["payload"]["text"] for item in entries if item["$"] == "before" - ] == [ - "Picking up tip from A1 of Opentrons 96 Tip Rack 1000 µL on 1", - "Aspirating 100.0 uL from A1 of Corning 96 Well Plate 360 µL Flat on 2 at 500.0 uL/sec", - "Dispensing 100.0 uL into B1 of Corning 96 Well Plate 360 µL Flat on 2 at 1000.0 uL/sec", - "Dropping tip into H12 of Opentrons 96 Tip Rack 1000 µL on 1", - ] + mock_get_attached_instr.return_value[types.Mount.LEFT] = { + "config": load_pipette_data.load_definition( + converted_model_v15.pipette_type, + converted_model_v15.pipette_channels, + converted_model_v15.pipette_version, + ), + "id": "testid", + } + mock_get_attached_instr.return_value[types.Mount.RIGHT] = { + "config": load_pipette_data.load_definition( + converted_model_v1.pipette_type, + converted_model_v1.pipette_channels, + converted_model_v1.pipette_version, + ), + "id": "testid2", + } + entries = [] + + def emit_runlog(entry: Any) -> None: + nonlocal entries + entries.append(entry) + + execute.execute(protocol.filelike, protocol.filename, emit_runlog=emit_runlog) + + # https://opentrons.atlassian.net/browse/RSS-320: + # PAPIv≥2.14 protocols currently emit JSON run log text, not human-readable text. + # Their exact contents can't be tested here because they're too verbose and they have + # unpredictable fields like `createdAt` and `id`. So as an approximation, we just test + # the command types. + command_types = [ + json.loads(item["payload"]["text"])["commandType"] + for item in entries + if item["$"] == "before" + ] + assert command_types == [ + "home", + "home", + "loadLabware", + "loadPipette", + "loadLabware", + "pickUpTip", + "aspirate", + "dispense", + "dropTip", + ] def test_execute_function_json_v3( diff --git a/api/tests/opentrons/util/test_broker.py b/api/tests/opentrons/util/test_broker.py index fc2abb8c2dc..35eca32d500 100644 --- a/api/tests/opentrons/util/test_broker.py +++ b/api/tests/opentrons/util/test_broker.py @@ -6,7 +6,32 @@ from opentrons.util.broker import Broker -def test_broker() -> None: +def test_context_manager_subscriptions() -> None: + """Test subscribing, receiving messages, and unsubscribing.""" + subject = Broker[int]() + + received_by_a: List[int] = [] + received_by_b: List[int] = [] + + def callback_a(message: int) -> None: + received_by_a.append(message) + + def callback_b(message: int) -> None: + received_by_b.append(message) + + subject.publish(1) + with subject.subscribed(callback_a): + subject.publish(2) + with subject.subscribed(callback_b): + subject.publish(3) + subject.publish(4) + subject.publish(5) + + assert received_by_a == [2, 3, 4] + assert received_by_b == [3] + + +def test_non_context_manager_subscriptions() -> None: """Test subscribing, receiving messages, and unsubscribing.""" subject = Broker[int]() From b8309e7d2eb18a626575e28db01630d3a15c3e38 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 28 Sep 2023 14:12:47 -0400 Subject: [PATCH 14/79] fix(g-code-testing): Update g code testing for new stop behavior (#13670) --- .../protocols/2.14/pcr_prep_part_2.txt | 25 ++++++++++++++++++- .../protocols/2.14/swift_smoke.txt | 25 ++++++++++++++++++- .../protocols/6/json_smoke.txt | 25 ++++++++++++++++++- .../comparison_files/protocols/6/no_mods.txt | 25 ++++++++++++++++++- 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/g-code-testing/g_code_test_data/comparison_files/protocols/2.14/pcr_prep_part_2.txt b/g-code-testing/g_code_test_data/comparison_files/protocols/2.14/pcr_prep_part_2.txt index ab73f754c1d..cc2d082c973 100644 --- a/g-code-testing/g_code_test_data/comparison_files/protocols/2.14/pcr_prep_part_2.txt +++ b/g-code-testing/g_code_test_data/comparison_files/protocols/2.14/pcr_prep_part_2.txt @@ -8775,6 +8775,29 @@ smoothie: G0 F24000.0 -> Setting speed to 24000.0 -> smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M204 S10000.0 A1500.0 B200.0 C200.0 X3000.0 Y2000.0 Z1500.0 -> Setting acceleration for the following axes: Default: 10000.0 X-Axis: 3000.0 Y-Axis: 2000.0 Left Pipette Arm: 1500.0 Right Pipette Arm: 1500.0 Left Pipette Suction: 200.0 Right Pipette Suction: 200.0 -> smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M999 -> Resetting OT-2 from error state -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G28.6 -> Getting homing status for all axes -> The homing status of the robot is: A Axis: 1 B Axis: 1 C Axis: 1 X Axis: 1 Y Axis: 1 Z Axis: 1 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M999 -> Resetting OT-2 from error state -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G28.6 -> Getting homing status for all axes -> The homing status of the robot is: A Axis: 1 B Axis: 1 C Axis: 1 X Axis: 1 Y Axis: 1 Z Axis: 1 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M92 X80.0 Y80.0 Z400.0 A400.0 -> Setting the following axes steps per mm: X-Axis: 80.0 steps per mm Y-Axis: 80.0 steps per mm Z-Axis: 400.0 steps per mm A-Axis: 400.0 steps per mm -> Current set steps per mm: +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M92 B768.0 C768.0 -> Setting the following axes steps per mm: B-Axis: 768.0 steps per mm C-Axis: 768.0 steps per mm -> Current set steps per mm: +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G90 -> Switching to Absolute Coordinate Mode -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M114.2 -> Getting current position for all axes -> The current position of the robot is: A Axis: 218.0 B Axis: -14.5 C Axis: -14.5 X Axis: 381.84 Y Axis: 351.5 Z Axis: 97.58 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M203.1 A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max speed for the following axes: X-Axis: 600.0 Y-Axis: 400.0 Z-Axis: 125.0 A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G0 F24000.0 -> Setting speed to 24000.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M204 S10000.0 A1500.0 B200.0 C200.0 X3000.0 Y2000.0 Z1500.0 -> Setting acceleration for the following axes: Default: 10000.0 X-Axis: 3000.0 Y-Axis: 2000.0 Left Pipette Arm: 1500.0 Right Pipette Arm: 1500.0 Left Pipette Suction: 200.0 Right Pipette Suction: 200.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M18 Z A B C -> Disengaging motor for the following axes: Z, A, B, C -> smoothie: M400 -> Waiting for motors to stop moving -> smoothie: G4 P0.1 -> Pausing movement for 0.1ms -> @@ -8926,4 +8949,4 @@ smoothie: G0 C-14.5 -> Moving the robot as follows: The right pipette suction to smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M907 A0.1 B0.3 C0.3 X0.3 Y0.3 Z0.1 -> Setting the current (in amps) to: X-Axis Motor: 0.3 Y-Axis Motor: 0.3 Z-Axis Motor: 0.1 A-Axis Motor: 0.1 B-Axis Motor: 0.3 C-Axis Motor: 0.3 -> smoothie: G4 P0.005 -> Pausing movement for 0.005ms -> -smoothie: M400 -> Waiting for motors to stop moving -> \ No newline at end of file +smoothie: M400 -> Waiting for motors to stop moving -> diff --git a/g-code-testing/g_code_test_data/comparison_files/protocols/2.14/swift_smoke.txt b/g-code-testing/g_code_test_data/comparison_files/protocols/2.14/swift_smoke.txt index 45af8bb51a1..4024047db0d 100644 --- a/g-code-testing/g_code_test_data/comparison_files/protocols/2.14/swift_smoke.txt +++ b/g-code-testing/g_code_test_data/comparison_files/protocols/2.14/swift_smoke.txt @@ -6991,6 +6991,29 @@ smoothie: G0 F24000.0 -> Setting speed to 24000.0 -> smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M204 S10000.0 A1500.0 B200.0 C200.0 X3000.0 Y2000.0 Z1500.0 -> Setting acceleration for the following axes: Default: 10000.0 X-Axis: 3000.0 Y-Axis: 2000.0 Left Pipette Arm: 1500.0 Right Pipette Arm: 1500.0 Left Pipette Suction: 200.0 Right Pipette Suction: 200.0 -> smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M999 -> Resetting OT-2 from error state -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G28.6 -> Getting homing status for all axes -> The homing status of the robot is: A Axis: 1 B Axis: 1 C Axis: 1 X Axis: 1 Y Axis: 1 Z Axis: 1 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M999 -> Resetting OT-2 from error state -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G28.6 -> Getting homing status for all axes -> The homing status of the robot is: A Axis: 1 B Axis: 1 C Axis: 1 X Axis: 1 Y Axis: 1 Z Axis: 1 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M92 X80.0 Y80.0 Z400.0 A400.0 -> Setting the following axes steps per mm: X-Axis: 80.0 steps per mm Y-Axis: 80.0 steps per mm Z-Axis: 400.0 steps per mm A-Axis: 400.0 steps per mm -> Current set steps per mm: +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M92 B768.0 C768.0 -> Setting the following axes steps per mm: B-Axis: 768.0 steps per mm C-Axis: 768.0 steps per mm -> Current set steps per mm: +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G90 -> Switching to Absolute Coordinate Mode -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M114.2 -> Getting current position for all axes -> The current position of the robot is: A Axis: 97.58 B Axis: -8.5 C Axis: -14.5 X Axis: 347.84 Y Axis: 351.5 Z Axis: 218.0 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M203.1 A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max speed for the following axes: X-Axis: 600.0 Y-Axis: 400.0 Z-Axis: 125.0 A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G0 F24000.0 -> Setting speed to 24000.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M204 S10000.0 A1500.0 B200.0 C200.0 X3000.0 Y2000.0 Z1500.0 -> Setting acceleration for the following axes: Default: 10000.0 X-Axis: 3000.0 Y-Axis: 2000.0 Left Pipette Arm: 1500.0 Right Pipette Arm: 1500.0 Left Pipette Suction: 200.0 Right Pipette Suction: 200.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M18 Z A B C -> Disengaging motor for the following axes: Z, A, B, C -> smoothie: M400 -> Waiting for motors to stop moving -> smoothie: G4 P0.1 -> Pausing movement for 0.1ms -> @@ -7126,4 +7149,4 @@ smoothie: G0 C-14.5 -> Moving the robot as follows: The right pipette suction to smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M907 A0.1 B0.05 C0.3 X0.3 Y0.3 Z0.1 -> Setting the current (in amps) to: X-Axis Motor: 0.3 Y-Axis Motor: 0.3 Z-Axis Motor: 0.1 A-Axis Motor: 0.1 B-Axis Motor: 0.05 C-Axis Motor: 0.3 -> smoothie: G4 P0.005 -> Pausing movement for 0.005ms -> -smoothie: M400 -> Waiting for motors to stop moving -> \ No newline at end of file +smoothie: M400 -> Waiting for motors to stop moving -> diff --git a/g-code-testing/g_code_test_data/comparison_files/protocols/6/json_smoke.txt b/g-code-testing/g_code_test_data/comparison_files/protocols/6/json_smoke.txt index e1b15deb785..e38cf8fd901 100644 --- a/g-code-testing/g_code_test_data/comparison_files/protocols/6/json_smoke.txt +++ b/g-code-testing/g_code_test_data/comparison_files/protocols/6/json_smoke.txt @@ -957,6 +957,29 @@ smoothie: G0 F24000.0 -> Setting speed to 24000.0 -> smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M204 S10000.0 A1500.0 B200.0 C200.0 X3000.0 Y2000.0 Z1500.0 -> Setting acceleration for the following axes: Default: 10000.0 X-Axis: 3000.0 Y-Axis: 2000.0 Left Pipette Arm: 1500.0 Right Pipette Arm: 1500.0 Left Pipette Suction: 200.0 Right Pipette Suction: 200.0 -> smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M999 -> Resetting OT-2 from error state -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G28.6 -> Getting homing status for all axes -> The homing status of the robot is: A Axis: 1 B Axis: 1 C Axis: 1 X Axis: 1 Y Axis: 1 Z Axis: 1 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M999 -> Resetting OT-2 from error state -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G28.6 -> Getting homing status for all axes -> The homing status of the robot is: A Axis: 1 B Axis: 1 C Axis: 1 X Axis: 1 Y Axis: 1 Z Axis: 1 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M92 X80.0 Y80.0 Z400.0 A400.0 -> Setting the following axes steps per mm: X-Axis: 80.0 steps per mm Y-Axis: 80.0 steps per mm Z-Axis: 400.0 steps per mm A-Axis: 400.0 steps per mm -> Current set steps per mm: +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M92 B768.0 C768.0 -> Setting the following axes steps per mm: B-Axis: 768.0 steps per mm C-Axis: 768.0 steps per mm -> Current set steps per mm: +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G90 -> Switching to Absolute Coordinate Mode -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M114.2 -> Getting current position for all axes -> The current position of the robot is: A Axis: 218.0 B Axis: -14.5 C Axis: -8.5 X Axis: 381.84 Y Axis: 351.5 Z Axis: 97.58 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M203.1 A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max speed for the following axes: X-Axis: 600.0 Y-Axis: 400.0 Z-Axis: 125.0 A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G0 F24000.0 -> Setting speed to 24000.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M204 S10000.0 A1500.0 B200.0 C200.0 X3000.0 Y2000.0 Z1500.0 -> Setting acceleration for the following axes: Default: 10000.0 X-Axis: 3000.0 Y-Axis: 2000.0 Left Pipette Arm: 1500.0 Right Pipette Arm: 1500.0 Left Pipette Suction: 200.0 Right Pipette Suction: 200.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M18 Z A B C -> Disengaging motor for the following axes: Z, A, B, C -> smoothie: M400 -> Waiting for motors to stop moving -> smoothie: G4 P0.1 -> Pausing movement for 0.1ms -> @@ -1092,4 +1115,4 @@ smoothie: G0 C-8.5 -> Moving the robot as follows: The right pipette suction to smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M907 A0.1 B0.3 C0.05 X0.3 Y0.3 Z0.1 -> Setting the current (in amps) to: X-Axis Motor: 0.3 Y-Axis Motor: 0.3 Z-Axis Motor: 0.1 A-Axis Motor: 0.1 B-Axis Motor: 0.3 C-Axis Motor: 0.05 -> smoothie: G4 P0.005 -> Pausing movement for 0.005ms -> -smoothie: M400 -> Waiting for motors to stop moving -> \ No newline at end of file +smoothie: M400 -> Waiting for motors to stop moving -> diff --git a/g-code-testing/g_code_test_data/comparison_files/protocols/6/no_mods.txt b/g-code-testing/g_code_test_data/comparison_files/protocols/6/no_mods.txt index 4ebf4daa085..13758552ea0 100644 --- a/g-code-testing/g_code_test_data/comparison_files/protocols/6/no_mods.txt +++ b/g-code-testing/g_code_test_data/comparison_files/protocols/6/no_mods.txt @@ -2092,6 +2092,29 @@ smoothie: G0 F24000.0 -> Setting speed to 24000.0 -> smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M204 S10000.0 A1500.0 B200.0 C200.0 X3000.0 Y2000.0 Z1500.0 -> Setting acceleration for the following axes: Default: 10000.0 X-Axis: 3000.0 Y-Axis: 2000.0 Left Pipette Arm: 1500.0 Right Pipette Arm: 1500.0 Left Pipette Suction: 200.0 Right Pipette Suction: 200.0 -> smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M999 -> Resetting OT-2 from error state -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G28.6 -> Getting homing status for all axes -> The homing status of the robot is: A Axis: 1 B Axis: 1 C Axis: 1 X Axis: 1 Y Axis: 1 Z Axis: 1 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M999 -> Resetting OT-2 from error state -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G28.6 -> Getting homing status for all axes -> The homing status of the robot is: A Axis: 1 B Axis: 1 C Axis: 1 X Axis: 1 Y Axis: 1 Z Axis: 1 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M92 X80.0 Y80.0 Z400.0 A400.0 -> Setting the following axes steps per mm: X-Axis: 80.0 steps per mm Y-Axis: 80.0 steps per mm Z-Axis: 400.0 steps per mm A-Axis: 400.0 steps per mm -> Current set steps per mm: +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M92 B768.0 C768.0 -> Setting the following axes steps per mm: B-Axis: 768.0 steps per mm C-Axis: 768.0 steps per mm -> Current set steps per mm: +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G90 -> Switching to Absolute Coordinate Mode -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M114.2 -> Getting current position for all axes -> The current position of the robot is: A Axis: 102.5 B Axis: -14.5 C Axis: -8.5 X Axis: 347.84 Y Axis: 351.5 Z Axis: 218.0 +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M203.1 A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max speed for the following axes: X-Axis: 600.0 Y-Axis: 400.0 Z-Axis: 125.0 A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: G0 F24000.0 -> Setting speed to 24000.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> +smoothie: M204 S10000.0 A1500.0 B200.0 C200.0 X3000.0 Y2000.0 Z1500.0 -> Setting acceleration for the following axes: Default: 10000.0 X-Axis: 3000.0 Y-Axis: 2000.0 Left Pipette Arm: 1500.0 Right Pipette Arm: 1500.0 Left Pipette Suction: 200.0 Right Pipette Suction: 200.0 -> +smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M18 Z A B C -> Disengaging motor for the following axes: Z, A, B, C -> smoothie: M400 -> Waiting for motors to stop moving -> smoothie: G4 P0.1 -> Pausing movement for 0.1ms -> @@ -2227,4 +2250,4 @@ smoothie: G0 C-8.5 -> Moving the robot as follows: The right pipette suction to smoothie: M400 -> Waiting for motors to stop moving -> smoothie: M907 A0.1 B0.3 C0.05 X0.3 Y0.3 Z0.1 -> Setting the current (in amps) to: X-Axis Motor: 0.3 Y-Axis Motor: 0.3 Z-Axis Motor: 0.1 A-Axis Motor: 0.1 B-Axis Motor: 0.3 C-Axis Motor: 0.05 -> smoothie: G4 P0.005 -> Pausing movement for 0.005ms -> -smoothie: M400 -> Waiting for motors to stop moving -> \ No newline at end of file +smoothie: M400 -> Waiting for motors to stop moving -> From 1879b24609a49c7357d95ce03b63a2327b63e866 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 28 Sep 2023 14:13:33 -0400 Subject: [PATCH 15/79] chore: add releases.json manifest for app (#13603) This adds a file that lives in the deploy bucket that tracks past releases of the app. We can use this for autogenerating previous-release pages and is a good thing to have. We can also use it for release revocation if we want. --- .github/workflows/app-test-build-deploy.yaml | 51 +++++++++++ scripts/update-releases-json.js | 90 ++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 scripts/update-releases-json.js diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index cee26372bf5..8b514727146 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -348,6 +348,7 @@ jobs: - name: 'deploy internal-release release builds to s3' run: | aws s3 --profile=deploy sync --acl=public-read to_upload_internal-release/ s3://${{ env._APP_DEPLOY_BUCKET_OT3 }}/${{ env._APP_DEPLOY_FOLDER_OT3 }} + - name: 'upload windows artifacts to GH release' uses: 'ncipollo/release-action@v1.12.0' if: needs.determine-build-type.outputs.type == 'release' @@ -412,3 +413,53 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ secrets.OT_APP_ROBOTSTACK_SLACK_NOTIFICATION_WEBHOOK_URL }} _ACCESS_URL: https://${{env._APP_DEPLOY_BUCKET_ROBOTSTACK}}/${{env._APP_DEPLOY_FOLDER_ROBOTSTACK}} + + - name: 'pull repo for scripts' + uses: 'actions/checkout@v3' + with: + path: ./monorepo + # https://github.com/actions/checkout/issues/290 + - name: 'Fix actions/checkout odd handling of tags' + if: startsWith(github.ref, 'refs/tags') + run: | + cd ./monorepo + git fetch -f origin ${{ github.ref }}:${{ github.ref }} + git checkout ${{ github.ref }} + - uses: 'actions/setup-node@v3' + with: + node-version: '16' + - name: 'install udev' + run: sudo apt-get update && sudo apt-get install libudev-dev + - name: 'set complex environment variables' + id: 'set-vars' + uses: actions/github-script@v6 + with: + script: | + const { buildComplexEnvVars } = require(`${process.env.GITHUB_WORKSPACE}/monorepo/.github/workflows/utils.js`) + buildComplexEnvVars(core, context) + - name: 'cache yarn cache' + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/.npm-cache/_prebuild + ${{ github.workspace }}/.yarn-cache + key: js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + - name: 'setup-js' + run: | + npm config set cache ${{ github.workspace }}/.npm-cache + yarn config set cache-folder ${{ github.workspace }}/.yarn-cache + cd monorepo + make setup-js + - name: 'update internal-releases releases.json' + if: needs.determine-build-type.outputs.type == 'release' && contains(fromJSON(needs.determine-build-type.outputs.variants), 'internal-release') + run: | + aws --profile=deploy s3 cp s3://${{ env._APP_DEPLOY_BUCKET_OT3 }}/${{ env._APP_DEPLOY_FOLDER_OT3 }}/releases.json ./to_upload_internal-release/releases.json + node ./monorepo/scripts/update-releases-json ./to_upload_internal-release/releases.json ot3 ./to_upload_internal-release https://ot3-development.builds.opentrons.com/app/ + aws --profile=deploy s3 cp ./to_upload_internal-release/releases.json s3://${{ env._APP_DEPLOY_BUCKET_OT3 }}/${{ env._APP_DEPLOY_FOLDER_OT3 }}/releases.json + + - name: 'update release releases.json' + if: needs.determine-build-type.outputs.type == 'release' && contains(fromJSON(needs.determine-build-type.outputs.variants), 'release') + run: | + aws --profile=deploy s3 cp s3://${{ env._APP_DEPLOY_BUCKET_ROBOTSTACK }}/${{ env._APP_DEPLOY_FOLDER_ROBOTSTACK }}/releases.json ./to_upload_release/releases.json + node ./monorepo/scripts/update-releases-json ./to_upload_release/releases.json robot-stack ./to_upload_release https://builds.opentrons.com/app/ + aws --profile=deploy s3 cp ./to_upload_release/releases.json s3://${{ env._APP_DEPLOY_BUCKET_ROBOTSTACK }}/${{ env._APP_DEPLOY_FOLDER_ROBOTSTACK }}/releases.json diff --git a/scripts/update-releases-json.js b/scripts/update-releases-json.js new file mode 100644 index 00000000000..3286256c42b --- /dev/null +++ b/scripts/update-releases-json.js @@ -0,0 +1,90 @@ +'use strict' + +const fs = require('fs/promises') + +// Updates a releases historical manifest with a release's version. + +const versionFinder = require('./git-version') + +const parseArgs = require('./deploy/lib/parseArgs') +const USAGE = + '\nUsage:\n node ./scripts/update-releases-json ' + +async function readOrDefaultReleases(releasesPath) { + try { + const releasesFile = await fs.readFile(releasesPath) + return JSON.parse(releasesFile) + } catch (error) { + console.log(`Could not read releases file: ${error}, defaulting`) + return { production: {} } + } +} + +const FILES_IN_RELEASE_JSON = [ + /Opentrons.*\.exe$/, + /Opentrons.*\.dmg$/, + /Opentrons.*\.AppImage$/, + /latest.*yml$/, + /alpha.*yml$/, + /beta.*yml$/, +] + +function artifactNameToObj(artifactName, urlBase) { + if (artifactName.search(/Opentrons.*\.exe$/) !== -1) { + return { 'Opentrons.exe': urlBase + artifactName } + } else if (artifactName.search(/Opentrons.*\.dmg$/) !== -1) { + return { 'Opentrons.dmg': urlBase + artifactName } + } else if (artifactName.search(/Opentrons.*\.AppImage$/) !== -1) { + return { 'Opentrons.AppImage': urlBase + artifactName } + } else if (artifactName.search(/(latest|alpha|beta).*yml$/) !== -1) { + return { [artifactName]: urlBase + artifactName } + } else { + throw new Error(`Unmatched artifact ${artifactName}`) + } +} + +async function artifactsFromDir(artifactDirPath, urlBase) { + const files = await fs.readdir(artifactDirPath, { withFileTypes: true }) + return files + .filter( + dirent => + dirent.isFile() && + FILES_IN_RELEASE_JSON.some(re => dirent.name.search(re) !== -1) + ) + .map(dirent => artifactNameToObj(dirent.name, urlBase)) + .reduce((prev, current) => ({ ...prev, ...current }), {}) +} + +async function main() { + const { args } = parseArgs(process.argv.slice(2)) + const [releasesPath, project, artifactDirPath, urlBase] = args + if (!releasesPath || !project || !artifactDirPath || !urlBase) { + throw new Error(USAGE) + } + console.log(`Updating ${releasesPath} with artifacts from ${artifactDirPath}`) + const releasesData = await readOrDefaultReleases(releasesPath) + const version = await versionFinder.versionForProject(project) + console.log(`Adding data for ${version}`) + releasesData.production[version] = { + ...(await artifactsFromDir( + artifactDirPath, + urlBase.endsWith('/') ? urlBase : `${urlBase}/` + )), + revoked: false, + } + console.log( + `Added ${Object.keys(releasesData.production[version]).length} artifacts` + ) + ;(await fs.open(releasesPath, 'w')).writeFile(JSON.stringify(releasesData)) +} + +if (require.main === module) { + main() + .then(() => { + console.log('release file updated') + }) + .catch(error => { + console.error('Release file update failed:', error.message) + process.exitCode = -1 + }) +} From 7060060f9677309c9f38ffcc40c693269e2296d4 Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 29 Sep 2023 11:22:52 -0400 Subject: [PATCH 16/79] fix(app): add null-check to all options without odd (#13684) * fix(app): add null-check to all options without odd --- .../AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx index 31598817eeb..7bb28cdc2e4 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx @@ -142,7 +142,8 @@ export function DeviceResetSlideout({ ).length // filtering out ODD setting because this gets implicitly cleared if all settings are selected - const allOptionsWithoutODD = options.filter(o => o.id !== 'onDeviceDisplay') + const allOptionsWithoutODD = + options != null ? options.filter(o => o.id !== 'onDeviceDisplay') : [] const isEveryOptionSelected = totalOptionsSelected > 0 && From adbccc9d18ae19adc005f1fa8689e08669041281 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:37:17 -0400 Subject: [PATCH 17/79] feat(app, labware-library): add adapter and adapter + labware combo imgs (#13680) closes RUXD-1109 --- .../images/deep_well_plate_adapter.jpg | Bin 0 -> 13583 bytes .../images/flat_bottom_aluminum.png | Bin 0 -> 282032 bytes .../images/flat_bottom_plate_adapter.jpg | Bin 0 -> 7366 bytes .../images/pcr_plate_adapter.jpg | Bin 0 -> 14604 bytes .../images/universal_flat_adapter.jpg | Bin 0 -> 18311 bytes .../LabwareDetails/labware-images.ts | 32 ++++++++++++++++++ .../components/labware-ui/labware-images.ts | 32 ++++++++++++++++++ .../src/images/deep_well_plate_adapter.jpg | Bin 0 -> 13583 bytes .../src/images/flat_bottom_aluminum.png | Bin 0 -> 282032 bytes .../src/images/flat_bottom_plate_adapter.jpg | Bin 0 -> 7366 bytes .../src/images/pcr_plate_adapter.jpg | Bin 0 -> 14604 bytes .../src/images/universal_flat_adapter.jpg | Bin 0 -> 18311 bytes 12 files changed, 64 insertions(+) create mode 100644 app/src/organisms/LabwareDetails/images/deep_well_plate_adapter.jpg create mode 100644 app/src/organisms/LabwareDetails/images/flat_bottom_aluminum.png create mode 100644 app/src/organisms/LabwareDetails/images/flat_bottom_plate_adapter.jpg create mode 100644 app/src/organisms/LabwareDetails/images/pcr_plate_adapter.jpg create mode 100644 app/src/organisms/LabwareDetails/images/universal_flat_adapter.jpg create mode 100644 labware-library/src/images/deep_well_plate_adapter.jpg create mode 100644 labware-library/src/images/flat_bottom_aluminum.png create mode 100644 labware-library/src/images/flat_bottom_plate_adapter.jpg create mode 100644 labware-library/src/images/pcr_plate_adapter.jpg create mode 100644 labware-library/src/images/universal_flat_adapter.jpg diff --git a/app/src/organisms/LabwareDetails/images/deep_well_plate_adapter.jpg b/app/src/organisms/LabwareDetails/images/deep_well_plate_adapter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c72d1c75c75fb046a16ca38e7a967b0288f9557 GIT binary patch literal 13583 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 literal 0 HcmV?d00001 diff --git a/app/src/organisms/LabwareDetails/images/flat_bottom_aluminum.png b/app/src/organisms/LabwareDetails/images/flat_bottom_aluminum.png new file mode 100644 index 0000000000000000000000000000000000000000..9d2566c6d3cc8415d061cfdb9b573619aa132da4 GIT binary patch literal 282032 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 z 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 be{U|E7ei<#$OTo%bCua0KV?;E=^6ij$>`cw literal 0 HcmV?d00001 diff --git a/app/src/organisms/LabwareDetails/images/flat_bottom_plate_adapter.jpg b/app/src/organisms/LabwareDetails/images/flat_bottom_plate_adapter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e1c5e76af6f0d7a2805a2e217ba1998581ee98b5 GIT binary patch literal 7366 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 D{==>7 literal 0 HcmV?d00001 diff --git a/app/src/organisms/LabwareDetails/images/universal_flat_adapter.jpg b/app/src/organisms/LabwareDetails/images/universal_flat_adapter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1ad27897a570edc18bac4bfc6b8fa8484c707787 GIT binary patch literal 18311 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 literal 0 HcmV?d00001 diff --git a/app/src/organisms/LabwareDetails/labware-images.ts b/app/src/organisms/LabwareDetails/labware-images.ts index dad9ca4979e..b6f70504ffa 100644 --- a/app/src/organisms/LabwareDetails/labware-images.ts +++ b/app/src/organisms/LabwareDetails/labware-images.ts @@ -210,4 +210,36 @@ export const labwareImages: Record = { biorad_384_wellplate_50ul: [ require('./images/biorad_384_wellplate_50ul.jpg'), ], + opentrons_96_deep_well_adapter: [ + require('./images/deep_well_plate_adapter.jpg'), + ], + opentrons_96_flat_bottom_adapter: [ + require('./images/flat_bottom_plate_adapter.jpg'), + ], + opentrons_96_pcr_adapter: [require('./images/pcr_plate_adapter.jpg')], + opentrons_universal_flat_adapter: [ + require('./images/universal_flat_adapter.jpg'), + ], + opentrons_aluminum_flat_bottom_plate: [ + require('./images/flat_bottom_aluminum.png'), + ], + opentrons_96_well_aluminum_block: [ + require('./images/opentrons_96_aluminumblock_side_view.jpg'), + ], + opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep: [ + require('./images/deep_well_plate_adapter.jpg'), + require('./images/nest_96_wellplate_2ml_deep.jpg'), + ], + opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat: [ + require('./images/flat_bottom_plate_adapter.jpg'), + require('./images/nest_96_wellplate_200ul_flat_three_quarters.jpg'), + ], + opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt: [ + require('./images/pcr_plate_adapter.jpg'), + require('./images/nest_96_wellplate_100ul_pcr_full_skirt_three_quarters.jpg'), + ], + opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat: [ + require('./images/universal_flat_adapter.jpg'), + require('./images/corning_384_wellplate_112ul_flat_photo_three_quarters.jpg'), + ], } diff --git a/labware-library/src/components/labware-ui/labware-images.ts b/labware-library/src/components/labware-ui/labware-images.ts index fe511a30154..a8c365b5014 100644 --- a/labware-library/src/components/labware-ui/labware-images.ts +++ b/labware-library/src/components/labware-ui/labware-images.ts @@ -210,4 +210,36 @@ export const labwareImages: Record = { biorad_384_wellplate_50ul: [ require('../../images/biorad_384_wellplate_50ul.jpg'), ], + opentrons_96_deep_well_adapter: [ + require('../../images/deep_well_plate_adapter.jpg'), + ], + opentrons_96_flat_bottom_adapter: [ + require('../../images/flat_bottom_plate_adapter.jpg'), + ], + opentrons_96_pcr_adapter: [require('../../images/pcr_plate_adapter.jpg')], + opentrons_universal_flat_adapter: [ + require('../../images/universal_flat_adapter.jpg'), + ], + opentrons_aluminum_flat_bottom_plate: [ + require('../../images/flat_bottom_aluminum.png'), + ], + opentrons_96_well_aluminum_block: [ + require('../../images/opentrons_96_aluminumblock_side_view.jpg'), + ], + opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep: [ + require('../../images/deep_well_plate_adapter.jpg'), + require('../../images/nest_96_wellplate_2ml_deep.jpg'), + ], + opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat: [ + require('../../images/flat_bottom_plate_adapter.jpg'), + require('../../images/nest_96_wellplate_200ul_flat_three_quarters.jpg'), + ], + opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt: [ + require('../../images/pcr_plate_adapter.jpg'), + require('../../images/nest_96_wellplate_100ul_pcr_full_skirt_three_quarters.jpg'), + ], + opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat: [ + require('../../images/universal_flat_adapter.jpg'), + require('../../images/corning_384_wellplate_112ul_flat_photo_three_quarters.jpg'), + ], } diff --git a/labware-library/src/images/deep_well_plate_adapter.jpg b/labware-library/src/images/deep_well_plate_adapter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c72d1c75c75fb046a16ca38e7a967b0288f9557 GIT binary patch literal 13583 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 literal 0 HcmV?d00001 diff --git a/labware-library/src/images/flat_bottom_aluminum.png b/labware-library/src/images/flat_bottom_aluminum.png new file mode 100644 index 0000000000000000000000000000000000000000..9d2566c6d3cc8415d061cfdb9b573619aa132da4 GIT binary patch literal 282032 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 z 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 be{U|E7ei<#$OTo%bCua0KV?;E=^6ij$>`cw literal 0 HcmV?d00001 diff --git a/labware-library/src/images/flat_bottom_plate_adapter.jpg b/labware-library/src/images/flat_bottom_plate_adapter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e1c5e76af6f0d7a2805a2e217ba1998581ee98b5 GIT binary patch literal 7366 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 D{==>7 literal 0 HcmV?d00001 diff --git a/labware-library/src/images/universal_flat_adapter.jpg b/labware-library/src/images/universal_flat_adapter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1ad27897a570edc18bac4bfc6b8fa8484c707787 GIT binary patch literal 18311 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 literal 0 HcmV?d00001 From 251547ee75a39e9b61f6441654c59cf16841df97 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:35:34 -0400 Subject: [PATCH 18/79] fix(app): nicknames on modules in protocol setup (#13695) closes RAUT-768 --- .../Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx | 7 +++---- .../Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index 75e5b246d15..cce216ed23a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -88,8 +88,8 @@ export function SetupLabwareMap({ labwareInAdapterInMod?.result?.labwareId ?? nestedLabwareId const topLabwareDisplayName = - labwareInAdapterInMod?.result?.definition.metadata - .displayName ?? nestedLabwareDisplayName + labwareInAdapterInMod?.params.displayName ?? + nestedLabwareDisplayName return ( Date: Mon, 2 Oct 2023 17:35:22 -0400 Subject: [PATCH 19/79] fix(app): fix text and buttons for gripper wizard flows (#13698) On the mount gripper step, we want to display a 'Need help?' link, but not a 'back' button. On the first pin attachment step (front jaw), we want to remove the 'back' button. --- .../localization/en/gripper_wizard_flows.json | 2 +- .../CurrentOffsetsTable.tsx | 2 +- .../organisms/GripperWizardFlows/MountGripper.tsx | 3 +-- app/src/organisms/GripperWizardFlows/MovePin.tsx | 3 ++- .../__tests__/BeforeBeginning.test.tsx | 4 ++-- .../__tests__/MountGripper.test.tsx | 14 +------------- .../GripperWizardFlows/__tests__/MovePin.test.tsx | 14 ++++++++++++-- 7 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/src/assets/localization/en/gripper_wizard_flows.json b/app/src/assets/localization/en/gripper_wizard_flows.json index 343f660779e..ea5da7baff7 100644 --- a/app/src/assets/localization/en/gripper_wizard_flows.json +++ b/app/src/assets/localization/en/gripper_wizard_flows.json @@ -20,7 +20,7 @@ "gripper_successfully_attached": "Gripper successfully attached", "gripper_successfully_calibrated": "Flex Gripper successfully calibrated", "gripper_successfully_detached": "Flex Gripper successfully detached", - "gripper": "Gripper", + "gripper": "Flex Gripper", "hex_screwdriver": "2.5 mm Hex Screwdriver", "hold_gripper_and_loosen_screws": "Hold the gripper in place and loosen the top gripper screw first. After that move onto the bottom screw. (The screws are captive and will not come apart from the gripper.) Then carefully remove the gripper.", "insert_pin_into_front_jaw": "Insert calibration pin in front jaw", diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx index c622b0ce252..48bfbd2fe44 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx @@ -137,7 +137,7 @@ export function CurrentOffsetsTable( justifyContent={JUSTIFY_SPACE_BETWEEN} padding={SPACING.spacing16} > - {t('applied_offset_data')} + {t('applied_offset_data')} {isLabwareOffsetCodeSnippetsOn ? ( { - const { proceed, isRobotMoving, goBack } = props + const { proceed, isRobotMoving } = props const { t } = useTranslation(['gripper_wizard_flows', 'shared']) const isOnDevice = useSelector(getIsOnDevice) const [showUnableToDetect, setShowUnableToDetect] = React.useState(false) @@ -140,7 +140,6 @@ export const MountGripper = ( proceedButtonText={t('continue')} proceedIsDisabled={isPending} proceed={handleOnClick} - back={goBack} /> ) } diff --git a/app/src/organisms/GripperWizardFlows/MovePin.tsx b/app/src/organisms/GripperWizardFlows/MovePin.tsx index 34bf6e02622..1a3d7350f14 100644 --- a/app/src/organisms/GripperWizardFlows/MovePin.tsx +++ b/app/src/organisms/GripperWizardFlows/MovePin.tsx @@ -243,6 +243,7 @@ export const MovePin = (props: MovePinProps): JSX.Element | null => { } /> ) + return errorMessage != null ? ( { proceedButtonText={buttonText} proceed={handleOnClick} proceedIsDisabled={maintenanceRunId == null} - back={goBack} + back={movement !== MOVE_PIN_TO_FRONT_JAW ? goBack : undefined} /> ) } diff --git a/app/src/organisms/GripperWizardFlows/__tests__/BeforeBeginning.test.tsx b/app/src/organisms/GripperWizardFlows/__tests__/BeforeBeginning.test.tsx index 2609b8376d5..31469271d1a 100644 --- a/app/src/organisms/GripperWizardFlows/__tests__/BeforeBeginning.test.tsx +++ b/app/src/organisms/GripperWizardFlows/__tests__/BeforeBeginning.test.tsx @@ -60,7 +60,7 @@ describe('BeforeBeginning', () => { getByText( 'Provided with robot. Using another size can strip the instrument’s screws.' ) - getByText('Gripper') + getByText('Flex Gripper') getByRole('button', { name: 'Move gantry to front' }).click() expect(props.chainRunCommands).toHaveBeenCalledWith( @@ -121,7 +121,7 @@ describe('BeforeBeginning', () => { ) getByText('You will need:') getByText('Calibration Pin') - getByText('Gripper') + getByText('Flex Gripper') // getByText('mock need help link') getByRole('button', { name: 'Move gantry to front' }).click() diff --git a/app/src/organisms/GripperWizardFlows/__tests__/MountGripper.test.tsx b/app/src/organisms/GripperWizardFlows/__tests__/MountGripper.test.tsx index e9876c91a48..1fbd96ac02a 100644 --- a/app/src/organisms/GripperWizardFlows/__tests__/MountGripper.test.tsx +++ b/app/src/organisms/GripperWizardFlows/__tests__/MountGripper.test.tsx @@ -20,13 +20,11 @@ describe('MountGripper', () => { props?: Partial> ) => ReturnType let mockRefetch: jest.Mock - let mockGoBack: jest.Mock let mockProceed: jest.Mock let mockChainRunCommands: jest.Mock let mockSetErrorMessage: jest.Mock beforeEach(() => { - mockGoBack = jest.fn() mockProceed = jest.fn() mockChainRunCommands = jest.fn() mockRefetch = jest.fn(() => Promise.resolve()) @@ -39,7 +37,7 @@ describe('MountGripper', () => { attachedGripper={props?.attachedGripper ?? null} chainRunCommands={mockChainRunCommands} isRobotMoving={false} - goBack={mockGoBack} + goBack={() => null} errorMessage={null} setErrorMessage={mockSetErrorMessage} {...props} @@ -83,16 +81,6 @@ describe('MountGripper', () => { expect(mockProceed).not.toHaveBeenCalled() }) - it('clicking go back calls back', () => { - mockUseInstrumentsQuery.mockReturnValue({ - refetch: mockRefetch, - data: null, - } as any) - const { getByLabelText } = render()[0] - getByLabelText('back').click() - expect(mockGoBack).toHaveBeenCalled() - }) - it('renders correct text', () => { mockUseInstrumentsQuery.mockReturnValue({ refetch: mockRefetch, diff --git a/app/src/organisms/GripperWizardFlows/__tests__/MovePin.test.tsx b/app/src/organisms/GripperWizardFlows/__tests__/MovePin.test.tsx index dbacda507e8..e70e260449c 100644 --- a/app/src/organisms/GripperWizardFlows/__tests__/MovePin.test.tsx +++ b/app/src/organisms/GripperWizardFlows/__tests__/MovePin.test.tsx @@ -97,8 +97,18 @@ describe('MovePin', () => { await expect(mockProceed).toHaveBeenCalled() }) - it('clicking go back calls back', () => { - const { getByLabelText } = render()[0] + it('clicking go back calls back on moving pin from front to rear jaw', () => { + const { getByLabelText } = render({ + movement: MOVE_PIN_FROM_FRONT_JAW_TO_REAR_JAW, + })[0] + getByLabelText('back').click() + expect(mockGoBack).toHaveBeenCalled() + }) + + it('clicking go back calls back on removing pin from rear jaw', () => { + const { getByLabelText } = render({ + movement: REMOVE_PIN_FROM_REAR_JAW, + })[0] getByLabelText('back').click() expect(mockGoBack).toHaveBeenCalled() }) From 6174230eb1e6e26adfb2b31fb9cea98a318975c0 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:39:03 -0400 Subject: [PATCH 20/79] fix(app): remove LED status bar disabling feature flag (#13700) --- .../Devices/RobotSettings/RobotSettingsFeatureFlags.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx index ffd2f00ca30..e538b099559 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx @@ -35,6 +35,7 @@ const NON_FEATURE_FLAG_SETTINGS = [ 'useOldAspirationFunctions', 'disableLogAggregation', 'disableFastProtocolUpload', + 'disableStatusBar', ] export function RobotSettingsFeatureFlags({ From ee77c49a56497e38e206a701db88cd01a7d73f6b Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 3 Oct 2023 16:15:12 -0400 Subject: [PATCH 21/79] fix(app,app-shell): undelete old robot update urls (#13701) We removed the urls for the old OT-2 infrastructure, because we don't want them anymore. But because we removed them from the app config, and the app doesn't downgrade configs, then if you - ran 7.0, the config gets deleted - run 6.3.1 or previous, the config never gets restored and the app just doesn't know where to get robot system updates and therefore acts as if there are none. That in turn means you can't downgrade your robot through the app, you have to manually update from zip. The gross but only solution is to change the config back to get those values to be present again when you downgrade. This leaves a historical hole - someone who ran 7.0.0 and downgraded to 6.3.1 would still have that behavior - but it's fixed going forward, and those affected can just remove their app config. --- .../src/config/__fixtures__/index.ts | 11 ++++ .../src/config/__tests__/migrate.test.ts | 31 +++++++---- app-shell-odd/src/config/migrate.ts | 18 ++++++- app-shell/src/config/__fixtures__/index.ts | 12 +++++ .../src/config/__tests__/migrate.test.ts | 51 +++++++++++-------- app-shell/src/config/migrate.ts | 29 +++++++++-- app/src/redux/config/schema-types.ts | 11 +++- 7 files changed, 125 insertions(+), 38 deletions(-) diff --git a/app-shell-odd/src/config/__fixtures__/index.ts b/app-shell-odd/src/config/__fixtures__/index.ts index b6f6d211253..08725e1cd2d 100644 --- a/app-shell-odd/src/config/__fixtures__/index.ts +++ b/app-shell-odd/src/config/__fixtures__/index.ts @@ -7,6 +7,7 @@ import type { ConfigV17, ConfigV18, ConfigV19, + ConfigV20, } from '@opentrons/app/src/redux/config/types' export const MOCK_CONFIG_V12: ConfigV12 = { @@ -118,3 +119,13 @@ export const MOCK_CONFIG_V19: ConfigV19 = { hasJustUpdated: false, }, } + +export const MOCK_CONFIG_V20: ConfigV20 = { + ...MOCK_CONFIG_V19, + version: 20, + robotSystemUpdate: { + manifestUrls: { + OT2: 'not-used-on-ODD', + }, + }, +} diff --git a/app-shell-odd/src/config/__tests__/migrate.test.ts b/app-shell-odd/src/config/__tests__/migrate.test.ts index bb197986621..b752b9437de 100644 --- a/app-shell-odd/src/config/__tests__/migrate.test.ts +++ b/app-shell-odd/src/config/__tests__/migrate.test.ts @@ -8,10 +8,11 @@ import { MOCK_CONFIG_V17, MOCK_CONFIG_V18, MOCK_CONFIG_V19, + MOCK_CONFIG_V20, } from '../__fixtures__' import { migrate } from '../migrate' -const NEWEST_VERSION = 19 +const NEWEST_VERSION = 20 describe('config migration', () => { it('should migrate version 12 to latest', () => { @@ -19,7 +20,7 @@ describe('config migration', () => { const result = migrate(v12Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 13 to latest', () => { @@ -27,7 +28,7 @@ describe('config migration', () => { const result = migrate(v13Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 14 to latest', () => { @@ -35,7 +36,7 @@ describe('config migration', () => { const result = migrate(v14Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 15 to latest', () => { @@ -43,7 +44,7 @@ describe('config migration', () => { const result = migrate(v15Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 16 to latest', () => { @@ -51,7 +52,7 @@ describe('config migration', () => { const result = migrate(v16Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 17 to latest', () => { @@ -59,22 +60,30 @@ describe('config migration', () => { const result = migrate(v17Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) - it('should keep version 18', () => { + it('should migration version 18 to latest', () => { const v18Config = MOCK_CONFIG_V18 const result = migrate(v18Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) - it('should keep version 19', () => { + it('should migration version 19 to latest', () => { const v19Config = MOCK_CONFIG_V19 const result = migrate(v19Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(v19Config) + expect(result).toEqual(MOCK_CONFIG_V20) + }) + + it('should keep version 20', () => { + const v20Config = MOCK_CONFIG_V20 + const result = migrate(v20Config) + + expect(result.version).toBe(NEWEST_VERSION) + expect(result).toEqual(v20Config) }) }) diff --git a/app-shell-odd/src/config/migrate.ts b/app-shell-odd/src/config/migrate.ts index 48d45f5cc3c..4aed0cdf1bf 100644 --- a/app-shell-odd/src/config/migrate.ts +++ b/app-shell-odd/src/config/migrate.ts @@ -13,6 +13,7 @@ import type { ConfigV17, ConfigV18, ConfigV19, + ConfigV20, } from '@opentrons/app/src/redux/config/types' // format // base config v12 defaults @@ -156,6 +157,18 @@ const toVersion19 = (prevConfig: ConfigV18): ConfigV19 => { return nextConfig } +const toVersion20 = (prevConfig: ConfigV19): ConfigV20 => { + return { + ...prevConfig, + version: 20 as const, + robotSystemUpdate: { + manifestUrls: { + OT2: 'not-used-on-ODD', + }, + }, + } +} + const MIGRATIONS: [ (prevConfig: ConfigV12) => ConfigV13, (prevConfig: ConfigV13) => ConfigV14, @@ -163,7 +176,8 @@ const MIGRATIONS: [ (prevConfig: ConfigV15) => ConfigV16, (prevConfig: ConfigV16) => ConfigV17, (prevConfig: ConfigV17) => ConfigV18, - (prevConfig: ConfigV18) => ConfigV19 + (prevConfig: ConfigV18) => ConfigV19, + (prevConfig: ConfigV19) => ConfigV20 ] = [ toVersion13, toVersion14, @@ -172,6 +186,7 @@ const MIGRATIONS: [ toVersion17, toVersion18, toVersion19, + toVersion20, ] export const DEFAULTS: Config = migrate(DEFAULTS_V12) @@ -186,6 +201,7 @@ export function migrate( | ConfigV17 | ConfigV18 | ConfigV19 + | ConfigV20 ): Config { let result = prevConfig // loop through the migrations, skipping any migrations that are unnecessary diff --git a/app-shell/src/config/__fixtures__/index.ts b/app-shell/src/config/__fixtures__/index.ts index 5225d825c74..848753aa993 100644 --- a/app-shell/src/config/__fixtures__/index.ts +++ b/app-shell/src/config/__fixtures__/index.ts @@ -19,6 +19,7 @@ import type { ConfigV17, ConfigV18, ConfigV19, + ConfigV20, } from '@opentrons/app/src/redux/config/types' export const MOCK_CONFIG_V0: ConfigV0 = { @@ -250,3 +251,14 @@ export const MOCK_CONFIG_V19: ConfigV19 = { hasJustUpdated: false, }, } + +export const MOCK_CONFIG_V20: ConfigV20 = { + ...MOCK_CONFIG_V19, + version: 20, + robotSystemUpdate: { + manifestUrls: { + OT2: + 'https://opentrons-buildroot-ci.s3.us-east-2.amazonaws.com/releases.json', + }, + }, +} diff --git a/app-shell/src/config/__tests__/migrate.test.ts b/app-shell/src/config/__tests__/migrate.test.ts index 0287fbb9e20..38bc6381f40 100644 --- a/app-shell/src/config/__tests__/migrate.test.ts +++ b/app-shell/src/config/__tests__/migrate.test.ts @@ -20,10 +20,11 @@ import { MOCK_CONFIG_V17, MOCK_CONFIG_V18, MOCK_CONFIG_V19, + MOCK_CONFIG_V20, } from '../__fixtures__' import { migrate } from '../migrate' -const NEWEST_VERSION = 19 +const NEWEST_VERSION = 20 describe('config migration', () => { it('should migrate version 0 to latest', () => { @@ -31,7 +32,7 @@ describe('config migration', () => { const result = migrate(v0Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 1 to latest', () => { @@ -39,7 +40,7 @@ describe('config migration', () => { const result = migrate(v1Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 2 to latest', () => { @@ -47,7 +48,7 @@ describe('config migration', () => { const result = migrate(v2Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 3 to latest', () => { @@ -55,7 +56,7 @@ describe('config migration', () => { const result = migrate(v3Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 4 to latest', () => { @@ -63,7 +64,7 @@ describe('config migration', () => { const result = migrate(v4Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 5 to latest', () => { @@ -71,7 +72,7 @@ describe('config migration', () => { const result = migrate(v5Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 6 to latest', () => { @@ -79,7 +80,7 @@ describe('config migration', () => { const result = migrate(v6Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 7 to latest', () => { @@ -87,7 +88,7 @@ describe('config migration', () => { const result = migrate(v7Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 8 to latest', () => { @@ -95,7 +96,7 @@ describe('config migration', () => { const result = migrate(v8Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 9 to latest', () => { @@ -103,7 +104,7 @@ describe('config migration', () => { const result = migrate(v9Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 10 to latest', () => { @@ -111,7 +112,7 @@ describe('config migration', () => { const result = migrate(v10Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 11 to latest', () => { @@ -119,7 +120,7 @@ describe('config migration', () => { const result = migrate(v11Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 12 to latest', () => { @@ -127,7 +128,7 @@ describe('config migration', () => { const result = migrate(v12Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 13 to latest', () => { @@ -135,7 +136,7 @@ describe('config migration', () => { const result = migrate(v13Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 14 to latest', () => { @@ -143,7 +144,7 @@ describe('config migration', () => { const result = migrate(v14Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 15 to latest', () => { @@ -151,7 +152,7 @@ describe('config migration', () => { const result = migrate(v15Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 16 to latest', () => { @@ -159,7 +160,7 @@ describe('config migration', () => { const result = migrate(v16Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 17 to latest', () => { @@ -167,20 +168,26 @@ describe('config migration', () => { const result = migrate(v17Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) it('should migrate version 18 to latest', () => { const v18Config = MOCK_CONFIG_V18 const result = migrate(v18Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V19) + expect(result).toEqual(MOCK_CONFIG_V20) }) - it('should keep version 19', () => { + it('should keep migrate version 19 to latest', () => { const v19Config = MOCK_CONFIG_V19 const result = migrate(v19Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(v19Config) + expect(result).toEqual(MOCK_CONFIG_V20) + }) + it('should keep version 20', () => { + const v20Config = MOCK_CONFIG_V20 + const result = migrate(v20Config) + expect(result.version).toBe(NEWEST_VERSION) + expect(result).toEqual(v20Config) }) }) diff --git a/app-shell/src/config/migrate.ts b/app-shell/src/config/migrate.ts index 0a4616e7b14..d13b26ba7a6 100644 --- a/app-shell/src/config/migrate.ts +++ b/app-shell/src/config/migrate.ts @@ -25,6 +25,7 @@ import type { ConfigV17, ConfigV18, ConfigV19, + ConfigV20, } from '@opentrons/app/src/redux/config/types' // format // base config v0 defaults @@ -42,7 +43,9 @@ export const DEFAULTS_V0: ConfigV0 = { }, buildroot: { - manifestUrl: 'not-used', + // do not rely on this value; it is present only for back compatibility + manifestUrl: + 'https://opentrons-buildroot-ci.s3.us-east-2.amazonaws.com/releases.json', }, // logging config @@ -258,7 +261,9 @@ const toVersion12 = (prevConfig: ConfigV11): ConfigV12 => { version: 12 as const, robotSystemUpdate: { manifestUrls: { - OT2: 'not-used', + // do not rely on this value; it is present only for back compatibility + OT2: + 'https://opentrons-buildroot-ci.s3.us-east-2.amazonaws.com/releases.json', OT3: 'not-used', }, }, @@ -354,6 +359,21 @@ const toVersion19 = (prevConfig: ConfigV18): ConfigV19 => { return nextConfig } +const toVersion20 = (prevConfig: ConfigV19): ConfigV20 => { + const nextConfig = { + ...prevConfig, + version: 20 as const, + robotSystemUpdate: { + manifestUrls: { + // do not rely on this value; it is present only for back compatibility + OT2: + 'https://opentrons-buildroot-ci.s3.us-east-2.amazonaws.com/releases.json', + }, + }, + } + return nextConfig +} + const MIGRATIONS: [ (prevConfig: ConfigV0) => ConfigV1, (prevConfig: ConfigV1) => ConfigV2, @@ -373,7 +393,8 @@ const MIGRATIONS: [ (prevConfig: ConfigV15) => ConfigV16, (prevConfig: ConfigV16) => ConfigV17, (prevConfig: ConfigV17) => ConfigV18, - (prevConfig: ConfigV18) => ConfigV19 + (prevConfig: ConfigV18) => ConfigV19, + (prevConfig: ConfigV19) => ConfigV20 ] = [ toVersion1, toVersion2, @@ -394,6 +415,7 @@ const MIGRATIONS: [ toVersion17, toVersion18, toVersion19, + toVersion20, ] export const DEFAULTS: Config = migrate(DEFAULTS_V0) @@ -420,6 +442,7 @@ export function migrate( | ConfigV17 | ConfigV18 | ConfigV19 + | ConfigV20 ): Config { const prevVersion = prevConfig.version let result = prevConfig diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index fe2230a5fcc..f7ba5e9bb05 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -226,4 +226,13 @@ export type ConfigV19 = Omit & { } } -export type Config = ConfigV19 +export type ConfigV20 = Omit & { + version: 20 + robotSystemUpdate: { + manifestUrls: { + OT2: string + } + } +} + +export type Config = ConfigV20 From c715cc4005e6100017612f91a56f35b163f930ee Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 3 Oct 2023 17:03:10 -0400 Subject: [PATCH 22/79] fix(robot-server): mutable configs for attached (#13696) When something hits /settings/pipettes, we load known mutable configuration overrides. So if you don't have any for the attached pipettes, we don't return anything. The thing is, what we actually want is to return all of the mutable configurations, with overrides applied. So now we do. Closes RET-1376 --- .../instruments/ot2/pipette_handler.py | 8 ++ .../protocols/instrument_configurer.py | 12 +++ .../service/legacy/routers/settings.py | 97 ++++++++++++++----- .../service/legacy/routers/test_settings.py | 81 +++++++++++++++- .../pipette/mutable_configurations.py | 74 +++++++++----- 5 files changed, 218 insertions(+), 54 deletions(-) diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index a73c0cdf5e2..e631a136e40 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -262,6 +262,14 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: def attached_instruments(self) -> Dict[MountType, PipetteDict]: return self.get_attached_instruments() + @property + def attached_pipettes(self) -> Dict[MountType, PipetteDict]: + return self.get_attached_instruments() + + @property + def get_attached_pipettes(self) -> Dict[MountType, PipetteDict]: + return self.get_attached_instruments() + @property def hardware_instruments(self) -> InstrumentsByMount[MountType]: """Do not write new code that uses this.""" diff --git a/api/src/opentrons/hardware_control/protocols/instrument_configurer.py b/api/src/opentrons/hardware_control/protocols/instrument_configurer.py index b4b95627321..820757e5e6b 100644 --- a/api/src/opentrons/hardware_control/protocols/instrument_configurer.py +++ b/api/src/opentrons/hardware_control/protocols/instrument_configurer.py @@ -77,6 +77,18 @@ def get_attached_instrument(self, mount: Mount) -> PipetteDict: def attached_instruments(self) -> Dict[Mount, PipetteDict]: return self.get_attached_instruments() + def get_attached_pipettes(self) -> Dict[Mount, PipetteDict]: + """Get the status dicts of cached attached pipettes. + + Works like get_attached_instruments but for pipettes only - on the Flex, + there will be no gripper information here. + """ + ... + + @property + def attached_pipettes(self) -> Dict[Mount, PipetteDict]: + return self.get_attached_pipettes() + def calibrate_plunger( self, mount: Mount, diff --git a/robot-server/robot_server/service/legacy/routers/settings.py b/robot-server/robot_server/service/legacy/routers/settings.py index 80677588cb5..a9105e6c7db 100644 --- a/robot-server/robot_server/service/legacy/routers/settings.py +++ b/robot-server/robot_server/service/legacy/routers/settings.py @@ -6,9 +6,16 @@ from fastapi import APIRouter, Depends from opentrons_shared_data.errors import ErrorCodes -from opentrons.hardware_control import HardwareControlAPI +from opentrons.hardware_control import ( + HardwareControlAPI, + dev_types as hardware_dev_types, +) from opentrons.system import log_control -from opentrons_shared_data.pipette import mutable_configurations, types as pip_types +from opentrons_shared_data.pipette import ( + mutable_configurations, + types as pip_types, + pipette_load_name_conversions as pip_names, +) from opentrons.config import ( reset as reset_util, robot_configs, @@ -276,15 +283,24 @@ async def get_robot_settings( response_model_by_alias=True, response_model_exclude_unset=True, ) -async def get_pipette_settings() -> MultiPipetteSettings: +async def get_pipette_settings( + hardware: HardwareControlAPI = Depends(get_hardware), +) -> MultiPipetteSettings: res = {} + attached_pipettes = hardware.attached_pipettes for pipette_id in mutable_configurations.known_pipettes( get_opentrons_path("pipette_config_overrides_dir") ): # Have to convert to dict using by_alias due to bug in fastapi - res[pipette_id] = _pipette_settings_from_config( + res[pipette_id] = _pipette_settings_from_known_id( pipette_id, ) + for dct in attached_pipettes.values(): + if "pipette_id" not in dct: + continue + res[dct["pipette_id"]] = _pipette_settings_with_defaults_from_attached_pipette( + dct + ) return res @@ -298,16 +314,22 @@ async def get_pipette_settings() -> MultiPipetteSettings: status.HTTP_404_NOT_FOUND: {"model": LegacyErrorResponse}, }, ) -async def get_pipette_setting(pipette_id: str) -> PipetteSettings: - if pipette_id not in mutable_configurations.known_pipettes( +async def get_pipette_setting( + pipette_id: str, hardware: HardwareControlAPI = Depends(get_hardware) +) -> PipetteSettings: + attached_pipettes = hardware.attached_pipettes + known_ids = mutable_configurations.known_pipettes( get_opentrons_path("pipette_config_overrides_dir") - ): - raise LegacyErrorResponse( - message=f"{pipette_id} is not a valid pipette id", - errorCode=ErrorCodes.PIPETTE_NOT_PRESENT.value.code, - ).as_error(status.HTTP_404_NOT_FOUND) - r = _pipette_settings_from_config(pipette_id) - return r + ) + if pipette_id in known_ids: + return _pipette_settings_from_known_id(pipette_id) + for dct in attached_pipettes.values(): + if dct.get("pipette_id") == pipette_id: + return _pipette_settings_with_defaults_from_attached_pipette(dct) + raise LegacyErrorResponse( + message=f"{pipette_id} is not a valid pipette id", + errorCode=ErrorCodes.PIPETTE_NOT_PRESENT.value.code, + ).as_error(status.HTTP_404_NOT_FOUND) @router.patch( @@ -339,22 +361,13 @@ async def patch_pipette_setting( raise LegacyErrorResponse( message=str(e), errorCode=ErrorCodes.GENERAL_ERROR.value.code ).as_error(status.HTTP_412_PRECONDITION_FAILED) - r = _pipette_settings_from_config(pipette_id) + r = _pipette_settings_from_known_id(pipette_id) return r -def _pipette_settings_from_config(pipette_id: str) -> PipetteSettings: - """ - Create a PipetteSettings object from pipette config for single pipette - - :param pc: pipette config module - :param pipette_id: pipette id - :return: PipetteSettings object - """ - mutable_configs = mutable_configurations.list_mutable_configs( - pipette_serial_number=pipette_id, - pipette_override_path=get_opentrons_path("pipette_config_overrides_dir"), - ) +def _pipette_settings_from_mutable_configs( + mutable_configs: pip_types.OverrideType, +) -> PipetteSettings: converted_dict: Dict[str, Union[str, Dict[str, Any]]] = {} # TODO rather than doing this gross thing, we should # mess around with pydantic dataclasses. @@ -376,3 +389,35 @@ def _pipette_settings_from_config(pipette_id: str) -> PipetteSettings: ), fields=fields, ) + + +def _pipette_settings_from_known_id(pipette_id: str) -> PipetteSettings: + """ + Create a PipetteSettings object from pipette config for single pipette + + :param pc: pipette config module + :param pipette_id: pipette id + :return: PipetteSettings object + """ + mutable_configs = mutable_configurations.list_mutable_configs( + pipette_serial_number=pipette_id, + pipette_override_path=get_opentrons_path("pipette_config_overrides_dir"), + ) + return _pipette_settings_from_mutable_configs(mutable_configs) + + +def _pipette_settings_with_defaults_from_attached_pipette( + pipette_dict: hardware_dev_types.PipetteDict, +) -> PipetteSettings: + """ + Create a PipetteSettings object from a pipette dict from hardware + """ + pipette_id = pipette_dict["pipette_id"] + pipette_model = pipette_dict["model"] + pipette_modelversion = pip_names.convert_pipette_model(pipette_model) + mutable_configs = mutable_configurations.list_mutable_configs_with_defaults( + pipette_model=pipette_modelversion, + pipette_serial_number=pipette_id, + pipette_override_path=get_opentrons_path("pipette_config_overrides_dir"), + ) + return _pipette_settings_from_mutable_configs(mutable_configs) diff --git a/robot-server/tests/service/legacy/routers/test_settings.py b/robot-server/tests/service/legacy/routers/test_settings.py index 65acd3978aa..0f96946dac0 100644 --- a/robot-server/tests/service/legacy/routers/test_settings.py +++ b/robot-server/tests/service/legacy/routers/test_settings.py @@ -1,5 +1,5 @@ import logging -from mock import patch, call +from mock import patch, call, MagicMock from dataclasses import make_dataclass from typing import Generator from pathlib import Path @@ -10,7 +10,11 @@ from opentrons.config.reset import ResetOptionId from opentrons.config import advanced_settings -from opentrons_shared_data.pipette import types as pip_types +from opentrons_shared_data.pipette import ( + types as pip_types, + pipette_definition as pip_def, +) +from opentrons.types import Mount from robot_server import app @@ -124,6 +128,15 @@ def mock_list_mutable_configs(decoy: Decoy) -> Decoy: yield m +@pytest.fixture +def mock_list_mutable_configs_with_defaults(decoy: Decoy) -> Decoy: + with patch( + "opentrons_shared_data.pipette.mutable_configurations.list_mutable_configs_with_defaults", + new=decoy.mock(), + ) as m: + yield m + + @pytest.fixture def mock_save_overrides(decoy: Decoy) -> Decoy: with patch( @@ -142,6 +155,70 @@ def mock_get_opentrons_dir(decoy: Decoy) -> Decoy: yield m +def test_receive_attached_pipette_settings( + decoy: Decoy, + api_client, + mock_known_pipettes: Decoy, + mock_get_opentrons_dir: Decoy, + mock_list_mutable_configs_with_defaults: Decoy, + hardware: MagicMock, +) -> None: + decoy.when(mock_get_opentrons_dir("pipette_config_overrides_dir")).then_return( + "nope" + ) + decoy.when(mock_known_pipettes("nope")).then_return([]) + hardware.attached_pipettes = { + Mount.LEFT: {"pipette_id": "P12345", "model": "p20_multi_v3.5"} + } + decoy.when( + mock_list_mutable_configs_with_defaults( + pipette_model=pip_def.PipetteModelVersionType( + pip_types.PipetteModelType.p20, + pip_types.PipetteChannelType.EIGHT_CHANNEL, + pip_types.PipetteVersionType(3, 5), + ), + pipette_serial_number="P12345", + pipette_override_path="nope", + ) + ).then_return( + { + "pickUpCurrent": pip_types.MutableConfig.build( + **{ + "units": "mm", + "type": "float", + "min": 1.0, + "max": 3.0, + "default": 1.5, + "value": 1.2, + }, + name="pickUpCurrent", + ), + "quirks": { + "dropTipShake": pip_types.QuirkConfig(name="dropTipShake", value=True) + }, + "model": "p20_multi_v3.5", + } + ) + resp = api_client.get("/settings/pipettes") + assert resp.status_code == 200 + assert resp.json() == { + "P12345": { + "info": {"model": "p20_multi_v3.5", "name": ""}, + "fields": { + "pickUpCurrent": { + "units": "mm", + "type": "float", + "min": 1.0, + "max": 3.0, + "default": 1.5, + "value": 1.2, + }, + "quirks": {"dropTipShake": True}, + }, + }, + } + + def test_receive_pipette_settings( decoy: Decoy, api_client, diff --git a/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py b/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py index 40e4f8c6464..d77af0620d8 100644 --- a/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py +++ b/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py @@ -189,6 +189,28 @@ def known_pipettes(pipette_override_path: Path) -> List[str]: ] +def _load_full_mutable_configs( + pipette_model: PipetteModelVersionType, overrides: OverrideType +) -> OverrideType: + base_configs = load_definition( + pipette_model.pipette_type, + pipette_model.pipette_channels, + pipette_model.pipette_version, + ) + base_configs_dict = base_configs.dict(by_alias=True) + full_mutable_configs = _list_all_mutable_configs(overrides, base_configs_dict) + + if not full_mutable_configs.get("name"): + full_mutable_configs["name"] = str( + convert_to_pipette_name_type(cast(PipetteName, str(pipette_model))) + ) + + if not full_mutable_configs.get("model"): + full_mutable_configs["model"] = str(pipette_model) + + return full_mutable_configs + + def list_mutable_configs( pipette_serial_number: str, pipette_override_path: Path ) -> OverrideType: @@ -207,36 +229,36 @@ def list_mutable_configs( # This is mimicing behavior from the original file # which returned an empty dict if no on disk value was found. return mutable_configs - else: - serial_key_match = SERIAL_STUB_REGEX.match(pipette_serial_number) - - if serial_key_match: - serial_key = serial_key_match.group(0) - else: - serial_key = "" - pipette_model = convert_pipette_model( - cast(PipetteModel, PIPETTE_SERIAL_MODEL_LOOKUP[serial_key]) - ) - base_configs = load_definition( - pipette_model.pipette_type, - pipette_model.pipette_channels, - pipette_model.pipette_version, - ) - base_configs_dict = base_configs.dict(by_alias=True) - full_mutable_configs = _list_all_mutable_configs( - mutable_configs, base_configs_dict - ) + serial_key_match = SERIAL_STUB_REGEX.match(pipette_serial_number) - if not full_mutable_configs.get("name"): - full_mutable_configs["name"] = str( - convert_to_pipette_name_type(cast(PipetteName, str(pipette_model))) - ) + if serial_key_match: + serial_key = serial_key_match.group(0) + else: + serial_key = "" + pipette_model = convert_pipette_model( + cast(PipetteModel, PIPETTE_SERIAL_MODEL_LOOKUP[serial_key]) + ) + return _load_full_mutable_configs(pipette_model, mutable_configs) - if not full_mutable_configs.get("model"): - full_mutable_configs["model"] = str(pipette_model) - return full_mutable_configs +def list_mutable_configs_with_defaults( + pipette_model: PipetteModelVersionType, + pipette_serial_number: Optional[str], + pipette_override_path: Path, +) -> OverrideType: + """ + Returns dict of mutable configs only, with their defaults. + """ + mutable_configs: OverrideType = {} + if pipette_serial_number: + try: + mutable_configs = _load_available_overrides( + pipette_serial_number, pipette_override_path + ) + except FileNotFoundError: + pass + return _load_full_mutable_configs(pipette_model, mutable_configs) def load_with_mutable_configurations( From c90bbb75fac0bd9a96fed830775cde96be272b4f Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 3 Oct 2023 17:16:08 -0400 Subject: [PATCH 23/79] fix(app): render labware on module correctly in moveLabware modal (#13710) closes RAUT-772 --- .../MoveLabwareInterventionContent.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index 81c667ba048..52647a033a2 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -202,9 +202,17 @@ export function MoveLabwareInterventionContent({ backgroundItems={ <> {moduleRenderInfo.map( - ({ x, y, moduleId, moduleDef, nestedLabwareDef }) => ( + ({ + x, + y, + moduleId, + moduleDef, + nestedLabwareDef, + nestedLabwareId, + }) => ( - {nestedLabwareDef != null ? ( + {nestedLabwareDef != null && + nestedLabwareId !== command.params.labwareId ? ( ) : null} From 419d225a9c7eda67ed1eec84edc1e0680f58ab83 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 3 Oct 2023 17:24:08 -0400 Subject: [PATCH 24/79] fix(app): fix calibration result summary condition issue (#13714) * fix(app): fix calibration result summary condition issue --- .../CalibrationHealthCheckResults.tsx | 20 ++++++++++--------- .../CalibrationHealthCheckResults.test.tsx | 4 ++-- .../CheckCalibration/ResultsSummary/index.tsx | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/src/organisms/CheckCalibration/ResultsSummary/CalibrationHealthCheckResults.tsx b/app/src/organisms/CheckCalibration/ResultsSummary/CalibrationHealthCheckResults.tsx index c6c258158b1..41f41dcbeb5 100644 --- a/app/src/organisms/CheckCalibration/ResultsSummary/CalibrationHealthCheckResults.tsx +++ b/app/src/organisms/CheckCalibration/ResultsSummary/CalibrationHealthCheckResults.tsx @@ -13,11 +13,11 @@ import { StyledText } from '../../../atoms/text' import { StatusLabel } from '../../../atoms/StatusLabel' interface CalibrationHealthCheckResultsProps { - isCalibrationCompleted: boolean + isCalibrationRecommended: boolean } export const CalibrationHealthCheckResults = ({ - isCalibrationCompleted, + isCalibrationRecommended, }: CalibrationHealthCheckResultsProps): JSX.Element => { const { t } = useTranslation('robot_calibration') return ( @@ -25,17 +25,19 @@ export const CalibrationHealthCheckResults = ({ {t('calibration_health_check_results')} { let props: React.ComponentProps beforeEach(() => { props = { - isCalibrationCompleted: true, + isCalibrationRecommended: false, } }) @@ -35,7 +35,7 @@ describe('CalibrationHealthCheckResults', () => { }) it('should render title and warning StatusLabel when calibration results includes bad', () => { - props.isCalibrationCompleted = false + props.isCalibrationRecommended = true const { getByText, getByTestId } = render(props) getByText('Calibration recommended') expect(getByTestId('status_circle')).toHaveStyle( diff --git a/app/src/organisms/CheckCalibration/ResultsSummary/index.tsx b/app/src/organisms/CheckCalibration/ResultsSummary/index.tsx index 624f8946b91..54ba1e6110b 100644 --- a/app/src/organisms/CheckCalibration/ResultsSummary/index.tsx +++ b/app/src/organisms/CheckCalibration/ResultsSummary/index.tsx @@ -111,7 +111,7 @@ export function ResultsSummary( // check all calibration status // if all of them are good, this returns true. otherwise return false - const isCalibrationCompleted = (): boolean => { + const isCalibrationRecommended = (): boolean => { const isOffsetsBad = pipetteResultsBad(calibrationsByMount.left.calibration).offsetBad && pipetteResultsBad(calibrationsByMount.right.calibration).offsetBad @@ -130,7 +130,7 @@ export function ResultsSummary( > From af7c4d45dde74c23bfb5ba02c7ada61e8b524072 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Wed, 4 Oct 2023 16:06:27 -0400 Subject: [PATCH 25/79] feat(api): Rotate the module calibration offset if the module was physically moved and rotated. (#13441) --- .gitignore | 3 + .../hardware_control/module_control.py | 10 +-- .../modules/module_calibration.py | 49 ++++------- api/src/opentrons/hardware_control/ot3api.py | 12 ++- .../commands/calibration/calibrate_module.py | 13 ++- .../resources/module_data_provider.py | 26 ++++-- .../protocol_engine/state/geometry.py | 38 ++++++++- .../protocol_engine/state/modules.py | 48 +++++------ .../opentrons/protocol_engine/state/state.py | 4 +- api/src/opentrons/protocol_engine/types.py | 8 ++ .../calibration/test_calibrate_module.py | 15 +++- .../state/test_geometry_view.py | 85 +++++++++++++++++-- .../protocol_engine/state/test_module_view.py | 11 ++- 13 files changed, 226 insertions(+), 96 deletions(-) diff --git a/.gitignore b/.gitignore index 054df941b82..080a7efe590 100755 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,6 @@ opentrons-robot-app.tar.gz # local VERSION.json file when pushing to Flex *new_version_file.json + +# swap files +*.swp diff --git a/api/src/opentrons/hardware_control/module_control.py b/api/src/opentrons/hardware_control/module_control.py index 20b8017a262..d7c5f391ea1 100644 --- a/api/src/opentrons/hardware_control/module_control.py +++ b/api/src/opentrons/hardware_control/module_control.py @@ -231,10 +231,10 @@ def get_module_by_module_id( return found_module def load_module_offset( - self, module_type: ModuleType, module_id: str, slot: Optional[str] = None - ) -> ModuleCalibrationOffset: + self, module_type: ModuleType, module_id: str + ) -> Optional[ModuleCalibrationOffset]: log.info(f"Loading module offset for {module_type} {module_id}") - return load_module_calibration_offset(module_type, module_id, slot) + return load_module_calibration_offset(module_type, module_id) def save_module_offset( self, @@ -244,9 +244,9 @@ def save_module_offset( slot: str, offset: Point, instrument_id: Optional[str] = None, - ) -> ModuleCalibrationOffset: + ) -> Optional[ModuleCalibrationOffset]: log.info(f"Saving module {module} {module_id} offset: {offset} for slot {slot}") save_module_calibration_offset( offset, mount, slot, module, module_id, instrument_id ) - return load_module_calibration_offset(module, module_id, slot) + return load_module_calibration_offset(module, module_id) diff --git a/api/src/opentrons/hardware_control/modules/module_calibration.py b/api/src/opentrons/hardware_control/modules/module_calibration.py index 9fcafd28e08..3976a6c8816 100644 --- a/api/src/opentrons/hardware_control/modules/module_calibration.py +++ b/api/src/opentrons/hardware_control/modules/module_calibration.py @@ -9,7 +9,6 @@ save_module_calibration, ) from opentrons.calibration_storage.types import SourceType -from opentrons.config.robot_configs import default_module_calibration_offset from opentrons.hardware_control.modules.types import ModuleType from opentrons.hardware_control.types import OT3Mount @@ -26,7 +25,7 @@ class ModuleCalibrationOffset: module: ModuleType source: SourceType status: CalibrationStatus - slot: Optional[str] = None + slot: str mount: Optional[OT3Mount] = None instrument_id: Optional[str] = None last_modified: Optional[datetime] = None @@ -35,37 +34,26 @@ class ModuleCalibrationOffset: def load_module_calibration_offset( module_type: ModuleType, module_id: str, - slot: Optional[str] = None, -) -> ModuleCalibrationOffset: +) -> Optional[ModuleCalibrationOffset]: """Loads the calibration offset for a module.""" - # load default if module offset data do not exist - module_cal_obj = ModuleCalibrationOffset( - slot=slot, - offset=Point(*default_module_calibration_offset()), + module_offset_data = get_module_offset(module_type, module_id) + if not module_offset_data: + return None + return ModuleCalibrationOffset( module=module_type, module_id=module_id, - source=SourceType.default, - status=CalibrationStatus(), + slot=module_offset_data.slot, + mount=module_offset_data.mount, + offset=module_offset_data.offset, + last_modified=module_offset_data.lastModified, + instrument_id=module_offset_data.instrument_id, + source=module_offset_data.source, + status=CalibrationStatus( + markedAt=module_offset_data.status.markedAt, + markedBad=module_offset_data.status.markedBad, + source=module_offset_data.status.source, + ), ) - if module_id: - module_offset_data = get_module_offset(module_type, module_id) - if module_offset_data: - return ModuleCalibrationOffset( - module=module_type, - module_id=module_id, - slot=module_offset_data.slot, - mount=module_offset_data.mount, - offset=module_offset_data.offset, - last_modified=module_offset_data.lastModified, - instrument_id=module_offset_data.instrument_id, - source=module_offset_data.source, - status=CalibrationStatus( - markedAt=module_offset_data.status.markedAt, - markedBad=module_offset_data.status.markedBad, - source=module_offset_data.status.source, - ), - ) - return module_cal_obj def save_module_calibration_offset( @@ -77,8 +65,7 @@ def save_module_calibration_offset( instrument_id: Optional[str] = None, ) -> None: """Save the calibration offset for a given module.""" - if module_id: - save_module_calibration(offset, mount, slot, module, module_id, instrument_id) + save_module_calibration(offset, mount, slot, module, module_id, instrument_id) def load_all_module_calibrations() -> List[ModuleCalibrationOffset]: diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 6d48e3d7d61..6d7975af363 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2084,11 +2084,15 @@ async def save_module_offset( self._log.warning(f"Could not save calibration: unknown module {module_id}") return None # TODO (ba, 2023-03-22): gripper_id and pipette_id should probably be combined to instrument_id - instrument_id = None - if self._gripper_handler.has_gripper(): - instrument_id = self._gripper_handler.get_gripper().gripper_id - elif self._pipette_handler.has_pipette(mount): + if self._pipette_handler.has_pipette(mount): instrument_id = self._pipette_handler.get_pipette(mount).pipette_id + elif mount == OT3Mount.GRIPPER and self._gripper_handler.has_gripper(): + instrument_id = self._gripper_handler.get_gripper().gripper_id + else: + self._log.warning( + f"Could not save calibration: no instrument found for {mount}" + ) + return None module_type = module.MODULE_TYPE self._log.info( f"Saving module offset: {offset} for module {module_type.name} {module_id}." diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py index bd89c931d27..a3e8da549a7 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from ...state import StateView -from ...types import ModuleOffsetVector +from ...types import ModuleOffsetVector, DeckSlotLocation from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.types import OT3Mount @@ -46,6 +46,10 @@ class CalibrateModuleResult(BaseModel): ..., description="Offset of calibrated module." ) + location: DeckSlotLocation = Field( + ..., description="The deck slot this module was calibrated in." + ) + class CalibrateModuleImplementation( AbstractCommandImpl[CalibrateModuleParams, CalibrateModuleResult] @@ -67,7 +71,7 @@ async def execute(self, params: CalibrateModuleParams) -> CalibrateModuleResult: self._hardware_api, ) ot3_mount = OT3Mount.from_mount(params.mount) - slot = self._state_view.modules.get_location(params.moduleId).slotName.id + slot = self._state_view.modules.get_location(params.moduleId) module_serial = self._state_view.modules.get_serial_number(params.moduleId) # NOTE (ba, 2023-03-31): There are two wells for calibration labware definitions # well A1 represents the location calibration square center relative to the adapters bottom-left corner @@ -78,13 +82,14 @@ async def execute(self, params: CalibrateModuleParams) -> CalibrateModuleResult: # start the calibration module_offset = await calibration.calibrate_module( - ot3_api, ot3_mount, slot, module_serial, nominal_position + ot3_api, ot3_mount, slot.slotName.id, module_serial, nominal_position ) return CalibrateModuleResult( moduleOffset=ModuleOffsetVector( x=module_offset.x, y=module_offset.y, z=module_offset.z - ) + ), + location=slot, ) diff --git a/api/src/opentrons/protocol_engine/resources/module_data_provider.py b/api/src/opentrons/protocol_engine/resources/module_data_provider.py index 634104cbedc..a12b85ee5b3 100644 --- a/api/src/opentrons/protocol_engine/resources/module_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/module_data_provider.py @@ -5,7 +5,14 @@ ) from opentrons_shared_data.module import load_definition -from ..types import ModuleModel, ModuleDefinition, ModuleOffsetVector +from opentrons.types import DeckSlotName +from ..types import ( + ModuleModel, + ModuleDefinition, + ModuleOffsetVector, + ModuleOffsetData, + DeckSlotLocation, +) class ModuleDataProvider: @@ -18,15 +25,20 @@ def get_definition(model: ModuleModel) -> ModuleDefinition: return ModuleDefinition.parse_obj(data) @staticmethod - def load_module_calibrations() -> Dict[str, ModuleOffsetVector]: + def load_module_calibrations() -> Dict[str, ModuleOffsetData]: """Load the module calibration offsets.""" - module_calibrations: Dict[str, ModuleOffsetVector] = dict() + module_calibrations: Dict[str, ModuleOffsetData] = dict() calibration_data = load_all_module_calibrations() for calibration in calibration_data: # NOTE module_id is really the module serial number, change this - module_calibrations[calibration.module_id] = ModuleOffsetVector( - x=calibration.offset.x, - y=calibration.offset.y, - z=calibration.offset.z, + module_calibrations[calibration.module_id] = ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector( + x=calibration.offset.x, + y=calibration.offset.y, + z=calibration.offset.z, + ), + location=DeckSlotLocation( + slotName=DeckSlotName.from_primitive(calibration.slot), + ), ) return module_calibrations diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 6c90a9c670d..7c26be23098 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -1,5 +1,6 @@ """Geometry state getters.""" import enum +from numpy import array, dot from typing import Optional, List, Set, Tuple, Union, cast from opentrons.types import Point, DeckSlotName, MountType @@ -18,9 +19,10 @@ DeckSlotLocation, ModuleLocation, OnLabwareLocation, - ModuleOffsetVector, LabwareLocation, LabwareOffsetVector, + ModuleOffsetVector, + ModuleOffsetData, DeckType, CurrentWell, TipGeometry, @@ -186,13 +188,45 @@ def _get_labware_position_offset( f"Either it has been loaded off-deck or its been moved off-deck." ) + def _normalize_module_calibration_offset( + self, + module_location: DeckSlotLocation, + offset_data: Optional[ModuleOffsetData], + ) -> ModuleOffsetVector: + """Normalize the module calibration offset depending on the module location.""" + if not offset_data: + return ModuleOffsetVector(x=0, y=0, z=0) + offset = offset_data.moduleOffsetVector + calibrated_slot = offset_data.location.slotName + calibrated_slot_column = self.get_slot_column(calibrated_slot) + current_slot_column = self.get_slot_column(module_location.slotName) + # make sure that we have valid colums since we cant have modules in the middle of the deck + assert set([calibrated_slot_column, current_slot_column]).issubset( + {1, 3} + ), f"Module calibration offset is an invalid slot {calibrated_slot}" + + # Check if the module has moved from one side of the deck to the other + if calibrated_slot_column != current_slot_column: + # Since the module was rotated, the calibration offset vector needs to be rotated by 180 degrees along the z axis + saved_offset = array([offset.x, offset.y, offset.z]) + rotation_matrix = array([[-1, 0, 0], [0, -1, 0], [0, 0, 1]]) + new_offset = dot(saved_offset, rotation_matrix) # type: ignore[no-untyped-call] + offset = ModuleOffsetVector( + x=new_offset[0], y=new_offset[1], z=new_offset[2] + ) + return offset + def _get_calibrated_module_offset( self, location: LabwareLocation ) -> ModuleOffsetVector: """Get a labware location's underlying calibrated module offset, if it is on a module.""" if isinstance(location, ModuleLocation): module_id = location.moduleId - return self._modules.get_module_calibration_offset(module_id) + module_location = self._modules.get_location(module_id) + offset_data = self._modules.get_module_calibration_offset(module_id) + return self._normalize_module_calibration_offset( + module_location, offset_data + ) elif isinstance(location, DeckSlotLocation): return ModuleOffsetVector(x=0, y=0, z=0) elif isinstance(location, OnLabwareLocation): diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 438fba9aaa0..6ac289a6b79 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -35,6 +35,7 @@ LoadedModule, ModuleModel, ModuleOffsetVector, + ModuleOffsetData, ModuleType, ModuleDefinition, DeckSlotLocation, @@ -144,7 +145,7 @@ class ModuleState: substate_by_module_id: Dict[str, ModuleSubStateType] """Information about each module that's specific to the module type.""" - module_offset_by_serial: Dict[str, ModuleOffsetVector] + module_offset_by_serial: Dict[str, ModuleOffsetData] """Information about each modules offsets.""" @@ -154,7 +155,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions): _state: ModuleState def __init__( - self, module_calibration_offsets: Optional[Dict[str, ModuleOffsetVector]] = None + self, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None ) -> None: """Initialize a ModuleStore and its state.""" self._state = ModuleState( @@ -195,6 +196,7 @@ def _handle_command(self, command: Command) -> None: self._update_module_calibration( module_id=command.params.moduleId, module_offset=command.result.moduleOffset, + location=command.result.location, ) if isinstance( @@ -289,7 +291,10 @@ def _add_module_substate( ) def _update_module_calibration( - self, module_id: str, module_offset: ModuleOffsetVector + self, + module_id: str, + module_offset: ModuleOffsetVector, + location: DeckSlotLocation, ) -> None: module = self._state.hardware_by_module_id.get(module_id) if module: @@ -297,7 +302,10 @@ def _update_module_calibration( assert ( module_serial is not None ), "Expected a module SN and got None instead." - self._state.module_offset_by_serial[module_serial] = module_offset + self._state.module_offset_by_serial[module_serial] = ModuleOffsetData( + moduleOffsetVector=module_offset, + location=location, + ) def _handle_heater_shaker_commands( self, @@ -650,19 +658,10 @@ def get_dimensions(self, module_id: str) -> ModuleDimensions: """Get the specified module's dimensions.""" return self.get_definition(module_id).dimensions - def get_module_calibration_offset(self, module_id: str) -> ModuleOffsetVector: - """Get the stored module calibration offset.""" - module_serial = self.get(module_id).serialNumber - if module_serial is not None: - offset = self._state.module_offset_by_serial.get(module_serial) - if offset: - return offset - return ModuleOffsetVector(x=0, y=0, z=0) - def get_nominal_module_offset( self, module_id: str, deck_type: DeckType ) -> LabwareOffsetVector: - """Get the module's offset vector computed with slot transform.""" + """Get the module's nominal offset vector computed with slot transform.""" definition = self.get_definition(module_id) slot = self.get_location(module_id).slotName.id @@ -689,19 +688,14 @@ def get_nominal_module_offset( z=xformed[2], ) - def get_module_offset( - self, module_id: str, deck_type: DeckType - ) -> LabwareOffsetVector: - """Get the module's offset vector computed with slot transform and calibrated module offsets.""" - offset_vector = self.get_nominal_module_offset(module_id, deck_type) - - # add the calibrated module offset if there is one - cal_offset = self.get_module_calibration_offset(module_id) - return LabwareOffsetVector( - x=offset_vector.x + cal_offset.x, - y=offset_vector.y + cal_offset.y, - z=offset_vector.z + cal_offset.z, - ) + def get_module_calibration_offset( + self, module_id: str + ) -> Optional[ModuleOffsetData]: + """Get the calibration module offset.""" + module_serial = self.get(module_id).serialNumber + if module_serial: + return self._state.module_offset_by_serial.get(module_serial) + return None def get_overall_height(self, module_id: str) -> float: """Get the height of the module, excluding any labware loaded atop it.""" diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 761056bdc87..5fdbd394b32 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -7,7 +7,7 @@ from opentrons_shared_data.deck.dev_types import DeckDefinitionV3 -from opentrons.protocol_engine.types import ModuleOffsetVector +from opentrons.protocol_engine.types import ModuleOffsetData from opentrons.util.broker import ReadOnlyBroker from ..resources import DeckFixedLabware @@ -131,7 +131,7 @@ def __init__( deck_fixed_labware: Sequence[DeckFixedLabware], is_door_open: bool, change_notifier: Optional[ChangeNotifier] = None, - module_calibration_offsets: Optional[Dict[str, ModuleOffsetVector]] = None, + module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, ) -> None: """Initialize a StateStore and its substores. diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 288e644fb16..5c4843f0d00 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -380,6 +380,14 @@ class ModuleOffsetVector(BaseModel): z: float +@dataclass +class ModuleOffsetData: + """Module calibration offset data.""" + + moduleOffsetVector: ModuleOffsetVector + location: DeckSlotLocation + + class OverlapOffset(Vec3f): """Offset representing overlap space of one labware on top of another labware or module.""" diff --git a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_module.py b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_module.py index 9907473f0b9..a7821bd80e0 100644 --- a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_module.py +++ b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_module.py @@ -17,6 +17,7 @@ from opentrons.protocol_engine.types import ( DeckSlotLocation, ModuleOffsetVector, + ModuleOffsetData, ) from opentrons.hardware_control.types import OT3Mount @@ -61,7 +62,12 @@ async def test_calibrate_module_implementation( ) decoy.when( subject._state_view.modules.get_module_calibration_offset(module_id) - ).then_return(ModuleOffsetVector(x=0, y=0, z=0)) + ).then_return( + ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector(x=0, y=0, z=0), + location=location, + ) + ) decoy.when( subject._state_view.geometry.get_nominal_well_position( labware_id=labware_id, well_name="B1" @@ -80,7 +86,12 @@ async def test_calibrate_module_implementation( result = await subject.execute(params) assert result == CalibrateModuleResult( - moduleOffset=ModuleOffsetVector(x=3, y=4, z=6) + moduleOffset=ModuleOffsetVector( + x=3, + y=4, + z=6, + ), + location=location, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index b6740482d04..0e1204688af 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -20,6 +20,7 @@ ModuleLocation, OnLabwareLocation, ModuleOffsetVector, + ModuleOffsetData, LoadedLabware, LoadedModule, ModuleModel, @@ -162,7 +163,10 @@ def test_get_labware_parent_position_on_module( ) ).then_return(OverlapOffset(x=1, y=2, z=3)) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( - ModuleOffsetVector(x=2, y=3, z=4) + ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector(x=2, y=3, z=4), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + ) ) result = subject.get_labware_parent_position("labware-id") @@ -199,9 +203,6 @@ def test_get_labware_parent_position_on_labware( decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( Point(1, 2, 3) ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) decoy.when(labware_view.get("adapter-id")).then_return(adapter_data) decoy.when(labware_view.get_dimensions("adapter-id")).then_return( Dimensions(x=123, y=456, z=5) @@ -227,7 +228,10 @@ def test_get_labware_parent_position_on_labware( ).then_return(OverlapOffset(x=-3, y=-2, z=-1)) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( - ModuleOffsetVector(x=3, y=4, z=5) + ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector(x=3, y=4, z=5), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + ) ) result = subject.get_labware_parent_position("labware-id") @@ -235,6 +239,62 @@ def test_get_labware_parent_position_on_labware( assert result == Point(9, 12, 15) +def test_module_calibration_offset_rotation( + decoy: Decoy, + labware_view: LabwareView, + module_view: ModuleView, + ot2_standard_deck_def: DeckDefinitionV3, + subject: GeometryView, +) -> None: + """Return the rotated module calibration offset if the module was moved from one side of the deck to the other.""" + labware_data = LoadedLabware( + id="labware-id", + loadName="b", + definitionUri=uri_from_details(namespace="a", load_name="b", version=1), + location=ModuleLocation(moduleId="module-id"), + offsetId=None, + ) + + decoy.when(labware_view.get("labware-id")).then_return(labware_data) + decoy.when(module_view.get_location("module-id")).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) + ) + decoy.when(module_view.get_connected_model("module-id")).then_return( + ModuleModel.TEMPERATURE_MODULE_V2 + ) + decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( + ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector(x=2, y=3, z=4), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), + ) + ) + + # the module has not changed location after calibration, so there is no rotation + result = subject._get_calibrated_module_offset(ModuleLocation(moduleId="module-id")) + assert result == ModuleOffsetVector(x=2, y=3, z=4) + + # the module has changed from slot D1 to D3, so we should rotate the calibration offset 180 degrees along the z axis + decoy.when(module_view.get_location("module-id")).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + result = subject._get_calibrated_module_offset(ModuleLocation(moduleId="module-id")) + assert result == ModuleOffsetVector(x=-2, y=-3, z=4) + + # attempting to load the module calibration offset from an invalid slot in the middle of the deck (A2, B2, C2, D2) + # is not be allowed since you can't even load a module in the middle to perform a module calibration in the + # first place. So if someone manually edits the stored module calibration offset we will throw an assert error. + decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( + ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector(x=2, y=3, z=4), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D2), + ) + ) + with pytest.raises(AssertionError): + result = subject._get_calibrated_module_offset( + ModuleLocation(moduleId="module-id") + ) + + def test_get_labware_origin_position( decoy: Decoy, well_plate_def: LabwareDefinition, @@ -338,7 +398,10 @@ def test_get_module_labware_highest_z( ).then_return(LabwareOffsetVector(x=4, y=5, z=6)) decoy.when(module_view.get_height_over_labware("module-id")).then_return(0.5) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( - ModuleOffsetVector(x=0, y=0, z=0) + ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector(x=0, y=0, z=0), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + ) ) decoy.when(module_view.get_connected_model("module-id")).then_return( ModuleModel.MAGNETIC_MODULE_V2 @@ -642,7 +705,10 @@ def test_get_module_labware_well_position( ) ).then_return(LabwareOffsetVector(x=4, y=5, z=6)) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( - ModuleOffsetVector(x=0, y=0, z=0) + ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector(x=0, y=0, z=0), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + ) ) decoy.when(module_view.get_connected_model("module-id")).then_return( ModuleModel.MAGNETIC_MODULE_V2 @@ -1174,7 +1240,10 @@ def test_get_labware_grip_point_for_labware_on_module( ) ).then_return(OverlapOffset(x=10, y=20, z=30)) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( - ModuleOffsetVector(x=100, y=200, z=300) + ModuleOffsetData( + moduleOffsetVector=ModuleOffsetVector(x=100, y=200, z=300), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + ) ) decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_4)).then_return( Point(100, 200, 300) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index 80f053e12a0..5b83cda94f0 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -16,7 +16,7 @@ ModuleLocation, LabwareOffsetVector, DeckType, - ModuleOffsetVector, + ModuleOffsetData, HeaterShakerLatchStatus, LabwareMovementOffsetData, ) @@ -44,7 +44,7 @@ def make_module_view( requested_model_by_module_id: Optional[Dict[str, Optional[ModuleModel]]] = None, hardware_by_module_id: Optional[Dict[str, HardwareModule]] = None, substate_by_module_id: Optional[Dict[str, ModuleSubStateType]] = None, - module_offset_by_serial: Optional[Dict[str, ModuleOffsetVector]] = None, + module_offset_by_serial: Optional[Dict[str, ModuleOffsetData]] = None, ) -> ModuleView: """Get a module view test subject with the specified state.""" state = ModuleState( @@ -325,7 +325,8 @@ def test_get_module_offset_for_ot2_standard( }, ) assert ( - subject.get_module_offset("module-id", DeckType.OT2_STANDARD) == expected_offset + subject.get_nominal_module_offset("module-id", DeckType.OT2_STANDARD) + == expected_offset ) @@ -379,7 +380,9 @@ def test_get_module_offset_for_ot3_standard( ) }, ) - result_offset = subject.get_module_offset("module-id", DeckType.OT3_STANDARD) + result_offset = subject.get_nominal_module_offset( + "module-id", DeckType.OT3_STANDARD + ) assert (result_offset.x, result_offset.y, result_offset.z) == pytest.approx( (expected_offset.x, expected_offset.y, expected_offset.z) ) From 2ffff9414dab8368c7379dc0b6136bf14b4bacd4 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Wed, 4 Oct 2023 16:18:58 -0400 Subject: [PATCH 26/79] fix(api, robot-server, shared-data): Flex and OT2 door check disparity correction (#13720) Fixes issue where OT2 would refuse to run with the door open --- api/src/opentrons/config/feature_flags.py | 6 ++---- .../maintenance_runs/maintenance_engine_store.py | 5 ++++- robot-server/robot_server/robot/control/router.py | 8 ++++++-- robot-server/robot_server/runs/engine_store.py | 5 ++++- .../python/opentrons_shared_data/robot/dev_types.py | 9 +++++++++ 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/api/src/opentrons/config/feature_flags.py b/api/src/opentrons/config/feature_flags.py index f46674fb56e..5bf289a49d2 100644 --- a/api/src/opentrons/config/feature_flags.py +++ b/api/src/opentrons/config/feature_flags.py @@ -20,10 +20,8 @@ def use_old_aspiration_functions() -> bool: ) -def enable_door_safety_switch() -> bool: - return advs.get_setting_with_env_overload( - "enableDoorSafetySwitch", RobotTypeEnum.FLEX - ) +def enable_door_safety_switch(robot_type: RobotTypeEnum) -> bool: + return advs.get_setting_with_env_overload("enableDoorSafetySwitch", robot_type) def disable_fast_protocol_upload() -> bool: diff --git a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py index 42081dbf459..9349b65f6d1 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py @@ -4,6 +4,7 @@ from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.robot.dev_types import RobotTypeEnum from opentrons.config import feature_flags from opentrons.hardware_control import HardwareControlAPI @@ -139,7 +140,9 @@ async def create( config=ProtocolEngineConfig( robot_type=self._robot_type, deck_type=self._deck_type, - block_on_door_open=feature_flags.enable_door_safety_switch(), + block_on_door_open=feature_flags.enable_door_safety_switch( + RobotTypeEnum.robot_literal_to_enum(self._robot_type) + ), ), ) diff --git a/robot-server/robot_server/robot/control/router.py b/robot-server/robot_server/robot/control/router.py index 4413374a15c..0193dce94f1 100644 --- a/robot-server/robot_server/robot/control/router.py +++ b/robot-server/robot_server/robot/control/router.py @@ -2,6 +2,10 @@ from fastapi import APIRouter, status, Depends from typing import TYPE_CHECKING +from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.robot.dev_types import RobotTypeEnum +from robot_server.hardware import get_robot_type + from robot_server.errors import ErrorBody from robot_server.errors.robot_errors import NotSupportedOnOT2 from robot_server.service.json_api import ( @@ -67,8 +71,8 @@ async def put_acknowledge_estop_disengage( return await _get_estop_status_response(estop_handler) -def get_door_switch_required() -> bool: - return ff.enable_door_safety_switch() +def get_door_switch_required(robot_type: RobotType = Depends(get_robot_type)) -> bool: + return ff.enable_door_safety_switch(RobotTypeEnum.robot_literal_to_enum(robot_type)) @control_router.get( diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index 40fd5d343ca..6faff1e6e1f 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -3,6 +3,7 @@ from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.robot.dev_types import RobotTypeEnum from opentrons.config import feature_flags from opentrons.hardware_control import HardwareControlAPI @@ -163,7 +164,9 @@ async def create( config=ProtocolEngineConfig( robot_type=self._robot_type, deck_type=self._deck_type, - block_on_door_open=feature_flags.enable_door_safety_switch(), + block_on_door_open=feature_flags.enable_door_safety_switch( + RobotTypeEnum.robot_literal_to_enum(self._robot_type) + ), ), ) runner = create_protocol_runner( diff --git a/shared-data/python/opentrons_shared_data/robot/dev_types.py b/shared-data/python/opentrons_shared_data/robot/dev_types.py index 5daff4ec29e..555ec6008ba 100644 --- a/shared-data/python/opentrons_shared_data/robot/dev_types.py +++ b/shared-data/python/opentrons_shared_data/robot/dev_types.py @@ -19,6 +19,15 @@ class RobotTypeEnum(enum.Enum): OT2 = enum.auto() FLEX = enum.auto() + @classmethod + def robot_literal_to_enum(cls, robot_type: RobotType) -> "RobotTypeEnum": + """Convert Robot Type Literal to Robot Type Enum.""" + if robot_type == "OT-2 Standard": + return cls.OT2 + elif robot_type == "OT-3 Standard": + return cls.FLEX + # No final `else` statement, depend on mypy exhaustiveness checking + class RobotDefinition(TypedDict): """A python version of the robot definition type.""" From 44432042a1275755ad23a896c32fe34bb64a4c14 Mon Sep 17 00:00:00 2001 From: Jamey H Date: Wed, 4 Oct 2023 16:43:08 -0400 Subject: [PATCH 27/79] fix(app): fix conditional rendering of cal dashboard completion screen (#13721) --- .../localization/en/robot_calibration.json | 2 +- app/src/organisms/CalibrateDeck/index.tsx | 9 ++++++--- app/src/organisms/CalibrateDeck/types.ts | 2 +- .../__tests__/CalibrationTaskList.test.tsx | 20 +++++++++---------- .../organisms/CalibrationTaskList/index.tsx | 10 +++++----- .../hooks/useDashboardCalibrateDeck.tsx | 6 +++--- .../Devices/CalibrationDashboard/index.tsx | 4 ++-- 7 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/src/assets/localization/en/robot_calibration.json b/app/src/assets/localization/en/robot_calibration.json index 44f7ca2049b..9828ed706c4 100644 --- a/app/src/assets/localization/en/robot_calibration.json +++ b/app/src/assets/localization/en/robot_calibration.json @@ -21,7 +21,6 @@ "calibration_recommended": "Calibration recommended", "calibration_status": "Calibration Status", "calibration_status_description": "For accurate and precise movement, calibrate the robot's deck, pipette offsets, and tip lengths.", - "calibrations_aborted": "Using current calibrations.", "calibrations_complete": "Calibrations complete!", "change_tip_rack": "Change tip rack", "check_tip_on_block": "Check tip on block", @@ -130,6 +129,7 @@ "unknown_custom_tiprack": "unknown custom tip rack", "use_calibration_block": "Use Calibration Block", "use_trash_bin": "Use trash bin", + "using_current_calibrations": "Using current calibrations.", "you_can_remove_cal_block": "You can remove the Calibration Block from the deck now.", "you_will_need": "You will need:" } diff --git a/app/src/organisms/CalibrateDeck/index.tsx b/app/src/organisms/CalibrateDeck/index.tsx index 8fa74d35718..fd6daaaa4e6 100644 --- a/app/src/organisms/CalibrateDeck/index.tsx +++ b/app/src/organisms/CalibrateDeck/index.tsx @@ -65,7 +65,7 @@ export function CalibrateDeck( dispatchRequests, showSpinner, isJogging, - wasExitBeforeCompletion, + exitBeforeDeckConfigCompletion, offsetInvalidationHandler, } = props const { currentStep, instrument, labware, supportedCommands } = @@ -97,8 +97,11 @@ export function CalibrateDeck( } function cleanUpAndExit(): void { - if (wasExitBeforeCompletion) { - wasExitBeforeCompletion.current = true + if ( + exitBeforeDeckConfigCompletion && + currentStep !== Sessions.DECK_STEP_CALIBRATION_COMPLETE + ) { + exitBeforeDeckConfigCompletion.current = true } if (session?.id) { dispatchRequests( diff --git a/app/src/organisms/CalibrateDeck/types.ts b/app/src/organisms/CalibrateDeck/types.ts index fb5ae8dcb10..b2df75e4a6d 100644 --- a/app/src/organisms/CalibrateDeck/types.ts +++ b/app/src/organisms/CalibrateDeck/types.ts @@ -7,6 +7,6 @@ export interface CalibrateDeckParentProps { dispatchRequests: DispatchRequestsType showSpinner: boolean isJogging: boolean - wasExitBeforeCompletion?: MutableRefObject + exitBeforeDeckConfigCompletion?: MutableRefObject offsetInvalidationHandler?: () => void } diff --git a/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx b/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx index 2bf0b7cfddc..9942ca681b3 100644 --- a/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx +++ b/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx @@ -43,7 +43,7 @@ const render = (robotName: string = 'otie') => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> , { @@ -94,7 +94,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> ) @@ -120,7 +120,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> ) @@ -145,7 +145,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> ) @@ -170,7 +170,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> ) @@ -194,7 +194,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> ) @@ -218,7 +218,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> ) @@ -240,7 +240,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={true} + exitBeforeDeckConfigCompletion={true} /> ) @@ -265,7 +265,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> ) @@ -293,7 +293,7 @@ describe('CalibrationTaskList', () => { pipOffsetCalLauncher={mockPipOffsetCalLauncher} tipLengthCalLauncher={mockTipLengthCalLauncher} deckCalLauncher={mockDeckCalLauncher} - wasExitBeforeCompletion={false} + exitBeforeDeckConfigCompletion={false} /> ) diff --git a/app/src/organisms/CalibrationTaskList/index.tsx b/app/src/organisms/CalibrationTaskList/index.tsx index 618ceabe68a..74a513778d5 100644 --- a/app/src/organisms/CalibrationTaskList/index.tsx +++ b/app/src/organisms/CalibrationTaskList/index.tsx @@ -35,7 +35,7 @@ interface CalibrationTaskListProps { pipOffsetCalLauncher: DashboardCalOffsetInvoker tipLengthCalLauncher: DashboardCalTipLengthInvoker deckCalLauncher: DashboardCalDeckInvoker - wasExitBeforeCompletion: boolean + exitBeforeDeckConfigCompletion: boolean } export function CalibrationTaskList({ @@ -43,7 +43,7 @@ export function CalibrationTaskList({ pipOffsetCalLauncher, tipLengthCalLauncher, deckCalLauncher, - wasExitBeforeCompletion, + exitBeforeDeckConfigCompletion, }: CalibrationTaskListProps): JSX.Element { const prevActiveIndex = React.useRef<[number, number] | null>(null) const [hasLaunchedWizard, setHasLaunchedWizard] = React.useState( @@ -127,14 +127,14 @@ export function CalibrationTaskList({ justifyContent={JUSTIFY_CENTER} alignItems={ALIGN_CENTER} > - {wasExitBeforeCompletion ? ( + {exitBeforeDeckConfigCompletion ? ( ) : ( )} - {wasExitBeforeCompletion - ? t('calibrations_aborted') + {exitBeforeDeckConfigCompletion + ? t('using_current_calibrations') : t('calibrations_complete')} (null) const createRequestId = React.useRef(null) const jogRequestId = React.useRef(null) - const wasExitBeforeCompletion = React.useRef(false) + const exitBeforeDeckConfigCompletion = React.useRef(false) const invalidateHandlerRef = React.useRef<(() => void) | undefined>() const { t } = useTranslation('robot_calibration') @@ -119,7 +119,7 @@ export function useDashboardCalibrateDeck( showSpinner={showSpinner} dispatchRequests={dispatchRequests} isJogging={isJogging} - wasExitBeforeCompletion={wasExitBeforeCompletion} + exitBeforeDeckConfigCompletion={exitBeforeDeckConfigCompletion} offsetInvalidationHandler={invalidateHandlerRef.current} /> )} @@ -131,6 +131,6 @@ export function useDashboardCalibrateDeck( return [ handleStartDashboardDeckCalSession, Wizard, - wasExitBeforeCompletion.current, + exitBeforeDeckConfigCompletion.current, ] } diff --git a/app/src/pages/Devices/CalibrationDashboard/index.tsx b/app/src/pages/Devices/CalibrationDashboard/index.tsx index fa13f935104..bcdd71cc31c 100644 --- a/app/src/pages/Devices/CalibrationDashboard/index.tsx +++ b/app/src/pages/Devices/CalibrationDashboard/index.tsx @@ -25,7 +25,7 @@ export function CalibrationDashboard(): JSX.Element { const [ dashboardDeckCalLauncher, DashboardDeckCalWizard, - wasExitBeforeCompletion, + exitBeforeDeckConfigCompletion, ] = useDashboardCalibrateDeck(robotName) return ( {DashboardDeckCalWizard} {DashboardOffsetCalWizard} From 685f04a2b109889ad45931f1341b2a8b145f9605 Mon Sep 17 00:00:00 2001 From: Brian Arthur Cooper Date: Thu, 5 Oct 2023 13:00:47 -0400 Subject: [PATCH 28/79] fix(app): add support link for manual intervention steps (#13715) Reintroduce the link to the desktop manual intervention modal that leads to a page explaining manual intervention steps. --- .../InterventionModal.stories.tsx | 46 +++++++++++++++++++ .../MoveLabwareInterventionContent.tsx | 1 - app/src/organisms/InterventionModal/index.tsx | 18 +++++--- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/app/src/organisms/InterventionModal/InterventionModal.stories.tsx b/app/src/organisms/InterventionModal/InterventionModal.stories.tsx index b181cfc5b4d..7f57ee9655f 100644 --- a/app/src/organisms/InterventionModal/InterventionModal.stories.tsx +++ b/app/src/organisms/InterventionModal/InterventionModal.stories.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' +import fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' import { configReducer } from '../../redux/config/reducer' import { mockRunData } from './__fixtures__' import { InterventionModal as InterventionModalComponent } from './' @@ -46,3 +47,48 @@ PauseIntervention.args = { command: pauseCommand, run: mockRunData, } + +export const MoveLabwareIntervention = Template.bind({}) +MoveLabwareIntervention.args = { + robotName: 'Otie', + command: { + commandType: 'moveLabware', + params: { + labwareId: 'fake_labware_id', + newLocation: { + slotName: '1', + }, + }, + }, + run: { + ...mockRunData, + labware: [ + { + id: 'fake_labware_id', + loadName: fixture_96_plate.parameters.loadName, + definitionUri: 'fixture/fixture_96_plate/1', + location: { + slotName: '9', + }, + }, + ], + }, + analysis: { + commands: [ + { + commandType: 'loadLabware', + params: { + displayName: 'fake display name', + labwareId: 'fake_labware_id', + loadName: fixture_96_plate.parameters.loadName, + namespace: 'fixture', + version: 1, + location: { slotName: '9' }, + }, + result: { + definition: fixture_96_plate, + }, + }, + ], + }, +} diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index 52647a033a2..c49ed451518 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -144,7 +144,6 @@ export function MoveLabwareInterventionContent({ movedLabwareDefUri != null ? labwareDefsByUri?.[movedLabwareDefUri] ?? null : null - if (oldLabwareLocation == null || movedLabwareDef == null) return null return ( {childContent} - {/* - TODO(BC, 08/31/23): reintroduce this link and justify space between when support article is written - + {t('protocol_info:manual_steps_learn_more')} - */} {t('confirm_and_resume')} From c98f91aa1cb86ce734821a337fd731ba1589863f Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:12:04 -0400 Subject: [PATCH 29/79] fix(app): style instrument and module slideouts (#13725) Change headers and content in slideout for current version and serial numbers for pipettes and modules. Adjust fontweight, colors, capitalization, and padding --- .../PipetteCard/AboutPipetteSlideout.tsx | 7 ++- .../__tests__/AboutPipetteSlideout.test.tsx | 4 +- .../GripperCard/AboutGripperSlideout.tsx | 5 +- .../__tests__/AboutGripperSlideout.test.tsx | 2 +- .../ModuleCard/AboutModuleSlideout.tsx | 27 +++++++---- .../__tests__/AboutModuleSlideout.test.tsx | 48 +++++++++---------- 6 files changed, 50 insertions(+), 43 deletions(-) diff --git a/app/src/organisms/Devices/PipetteCard/AboutPipetteSlideout.tsx b/app/src/organisms/Devices/PipetteCard/AboutPipetteSlideout.tsx index f022c4007ec..e1981dbaa40 100644 --- a/app/src/organisms/Devices/PipetteCard/AboutPipetteSlideout.tsx +++ b/app/src/organisms/Devices/PipetteCard/AboutPipetteSlideout.tsx @@ -59,12 +59,12 @@ export const AboutPipetteSlideout = ( fontWeight={TYPOGRAPHY.fontWeightSemiBold} color={COLORS.darkGreyEnabled} > - {t('current_version')} + {i18n.format(t('current_version'), 'upperCase')} {instrumentInfo.firmwareVersion} @@ -75,9 +75,8 @@ export const AboutPipetteSlideout = ( fontWeight={TYPOGRAPHY.fontWeightSemiBold} color={COLORS.darkGreyEnabled} data-testid={`AboutPipetteSlideout_serial_number_text_${pipetteId}`} - textTransform={TYPOGRAPHY.textTransformUppercase} > - {t('serial_number')} + {i18n.format(t('serial_number'), 'upperCase')} { getByText('About Left Pipette Pipette') getByText('123') - getByText('Serial Number') + getByText('SERIAL NUMBER') const button = getByRole('button', { name: /exit/i }) fireEvent.click(button) expect(props.onCloseClick).toHaveBeenCalled() @@ -63,7 +63,7 @@ describe('AboutPipetteSlideout', () => { const { getByText } = render(props) - getByText('Current Version') + getByText('CURRENT VERSION') getByText('12') }) }) diff --git a/app/src/organisms/GripperCard/AboutGripperSlideout.tsx b/app/src/organisms/GripperCard/AboutGripperSlideout.tsx index aa325526003..17691324083 100644 --- a/app/src/organisms/GripperCard/AboutGripperSlideout.tsx +++ b/app/src/organisms/GripperCard/AboutGripperSlideout.tsx @@ -47,12 +47,12 @@ export const AboutGripperSlideout = ( fontWeight={TYPOGRAPHY.fontWeightSemiBold} color={COLORS.darkGreyEnabled} > - {t('current_version')} + {i18n.format(t('current_version'), 'upperCase')} {firmwareVersion} @@ -62,7 +62,6 @@ export const AboutGripperSlideout = ( as="h6" fontWeight={TYPOGRAPHY.fontWeightSemiBold} color={COLORS.darkGreyEnabled} - textTransform={TYPOGRAPHY.textTransformUppercase} > {i18n.format(t('serial_number'), 'upperCase')} diff --git a/app/src/organisms/GripperCard/__tests__/AboutGripperSlideout.test.tsx b/app/src/organisms/GripperCard/__tests__/AboutGripperSlideout.test.tsx index 8b457b50116..b510a68ecd0 100644 --- a/app/src/organisms/GripperCard/__tests__/AboutGripperSlideout.test.tsx +++ b/app/src/organisms/GripperCard/__tests__/AboutGripperSlideout.test.tsx @@ -34,7 +34,7 @@ describe('AboutGripperSlideout', () => { props = { ...props, firmwareVersion: '12' } const { getByText } = render(props) - getByText('Current Version') + getByText('CURRENT VERSION') getByText('12') }) }) diff --git a/app/src/organisms/ModuleCard/AboutModuleSlideout.tsx b/app/src/organisms/ModuleCard/AboutModuleSlideout.tsx index 401fcbe82e4..a5e88fede9f 100644 --- a/app/src/organisms/ModuleCard/AboutModuleSlideout.tsx +++ b/app/src/organisms/ModuleCard/AboutModuleSlideout.tsx @@ -37,7 +37,7 @@ export const AboutModuleSlideout = ( props: AboutModuleSlideoutProps ): JSX.Element | null => { const { module, isExpanded, onCloseClick, firmwareUpdateClick } = props - const { t } = useTranslation(['device_details', 'shared']) + const { i18n, t } = useTranslation(['device_details', 'shared']) const moduleName = getModuleDisplayName(module.moduleModel) const runStatus = useCurrentRunStatus() const [showBanner, setShowBanner] = React.useState(true) @@ -97,26 +97,35 @@ export const AboutModuleSlideout = ( - {t('current_version')} - - {t('version', { version: module.firmwareVersion })} + + {i18n.format(t('current_version'), 'upperCase')} + + + {module.firmwareVersion} - {t('serial_number')} + {i18n.format(t('serial_number'), 'upperCase')} diff --git a/app/src/organisms/ModuleCard/__tests__/AboutModuleSlideout.test.tsx b/app/src/organisms/ModuleCard/__tests__/AboutModuleSlideout.test.tsx index 555eacf7c98..c52a4234ae7 100644 --- a/app/src/organisms/ModuleCard/__tests__/AboutModuleSlideout.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/AboutModuleSlideout.test.tsx @@ -49,9 +49,9 @@ describe('AboutModuleSlideout', () => { getByText('About Magnetic Module GEN1') getByText('def456') - getByText('Serial Number') - getByText('Current Version') - getByText('Version v2.0.0') + getByText('SERIAL NUMBER') + getByText('CURRENT VERSION') + getByText('v2.0.0') const button = getByRole('button', { name: /exit/i }) fireEvent.click(button) expect(props.onCloseClick).toHaveBeenCalled() @@ -63,9 +63,9 @@ describe('AboutModuleSlideout', () => { getByText('About Magnetic Module GEN1') getByText('def456') - getByText('Serial Number') - getByText('Current Version') - getByText('Version v2.0.0') + getByText('SERIAL NUMBER') + getByText('CURRENT VERSION') + getByText('v2.0.0') }) it('renders no banner when run is finishing', () => { @@ -74,9 +74,9 @@ describe('AboutModuleSlideout', () => { getByText('About Magnetic Module GEN1') getByText('def456') - getByText('Serial Number') - getByText('Current Version') - getByText('Version v2.0.0') + getByText('SERIAL NUMBER') + getByText('CURRENT VERSION') + getByText('v2.0.0') }) it('renders correct info when module is a magnetic module GEN2', () => { @@ -90,9 +90,9 @@ describe('AboutModuleSlideout', () => { getByText('About Magnetic Module GEN2') getByText('def456') - getByText('Serial Number') - getByText('Current Version') - getByText('Version v2.0.0') + getByText('SERIAL NUMBER') + getByText('CURRENT VERSION') + getByText('v2.0.0') }) it('renders correct info when module is a temperature module GEN2', () => { @@ -106,9 +106,9 @@ describe('AboutModuleSlideout', () => { getByText('About Temperature Module GEN2') getByText('abc123') - getByText('Serial Number') - getByText('Current Version') - getByText('Version v2.0.0') + getByText('SERIAL NUMBER') + getByText('CURRENT VERSION') + getByText('v2.0.0') }) it('renders correct info when module is a temperature module GEN1', () => { @@ -122,9 +122,9 @@ describe('AboutModuleSlideout', () => { getByText('About Temperature Module GEN1') getByText('abc123') - getByText('Serial Number') - getByText('Current Version') - getByText('Version v2.0.0') + getByText('SERIAL NUMBER') + getByText('CURRENT VERSION') + getByText('v2.0.0') }) it('renders correct info when module is a thermocycler module with an update available', () => { @@ -138,9 +138,9 @@ describe('AboutModuleSlideout', () => { getByText('About Thermocycler Module GEN1') getByText('ghi789') - getByText('Serial Number') - getByText('Current Version') - getByText('Version v2.0.0') + getByText('SERIAL NUMBER') + getByText('CURRENT VERSION') + getByText('v2.0.0') getByText('Firmware update available.') const viewUpdate = getByRole('button', { name: 'Update now' }) fireEvent.click(viewUpdate) @@ -163,9 +163,9 @@ describe('AboutModuleSlideout', () => { getByText('About Temperature Module GEN1') getByText('abc123') - getByText('Serial Number') - getByText('Current Version') - getByText('Version v2.0.0') + getByText('SERIAL NUMBER') + getByText('CURRENT VERSION') + getByText('v2.0.0') const button = getByRole('button', { name: 'close' }) fireEvent.click(button) expect(props.onCloseClick).toHaveBeenCalled() From e6889c5e7b46030863f81221f25d6401bfa78d69 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:16:59 -0400 Subject: [PATCH 30/79] fix(app): pipette recalibration banner conditional rendering and styling (#13727) Only render the pipette recal warning banner if the robot is viewable. Stretch banner to full width of instruments and modules flexbox if the window is expanded. Write test. --- .../Devices/InstrumentsAndModules.tsx | 7 +++--- .../PipetteRecalibrationWarning.tsx | 2 +- .../__tests__/InstrumentsAndModules.test.tsx | 24 ++++++++++++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/src/organisms/Devices/InstrumentsAndModules.tsx b/app/src/organisms/Devices/InstrumentsAndModules.tsx index bb700732a26..b87d1495c9a 100644 --- a/app/src/organisms/Devices/InstrumentsAndModules.tsx +++ b/app/src/organisms/Devices/InstrumentsAndModules.tsx @@ -182,9 +182,10 @@ export function InstrumentsAndModules({ {t('robot_control_not_available')} )} - {getShowPipetteCalibrationWarning(attachedInstruments) && - (currentRunId == null || isRunTerminal) ? ( - + {isRobotViewable && + getShowPipetteCalibrationWarning(attachedInstruments) && + (isRunTerminal || currentRunId == null) ? ( + ) : null} diff --git a/app/src/organisms/Devices/PipetteCard/PipetteRecalibrationWarning.tsx b/app/src/organisms/Devices/PipetteCard/PipetteRecalibrationWarning.tsx index decd6b35606..0ee79d620ff 100644 --- a/app/src/organisms/Devices/PipetteCard/PipetteRecalibrationWarning.tsx +++ b/app/src/organisms/Devices/PipetteCard/PipetteRecalibrationWarning.tsx @@ -16,7 +16,7 @@ export const PipetteRecalibrationWarning = (): JSX.Element | null => { if (!showBanner) return null return ( - + { @@ -41,6 +46,7 @@ jest.mock('../utils', () => { return { ...actualUtils, getIs96ChannelPipetteAttached: jest.fn(), + getShowPipetteCalibrationWarning: jest.fn(), } }) jest.mock('../../RunTimeControl/hooks') @@ -60,6 +66,9 @@ const mockUseAllPipetteOffsetCalibrationsQuery = useAllPipetteOffsetCalibrations const mockModuleCard = ModuleCard as jest.MockedFunction const mockPipetteCard = PipetteCard as jest.MockedFunction const mockGripperCard = GripperCard as jest.MockedFunction +const mockPipetteRecalibrationWarning = PipetteRecalibrationWarning as jest.MockedFunction< + typeof PipetteRecalibrationWarning +> const mockUsePipettesQuery = usePipettesQuery as jest.MockedFunction< typeof usePipettesQuery > @@ -73,6 +82,9 @@ const mockUseRunStatuses = useRunStatuses as jest.MockedFunction< const mockGetIs96ChannelPipetteAttached = getIs96ChannelPipetteAttached as jest.MockedFunction< typeof getIs96ChannelPipetteAttached > +const mockGetShowPipetteCalibrationWarning = getShowPipetteCalibrationWarning as jest.MockedFunction< + typeof getShowPipetteCalibrationWarning +> const mockUseIsOT3 = useIsOT3 as jest.MockedFunction const render = () => { @@ -91,12 +103,16 @@ describe('InstrumentsAndModules', () => { isRunTerminal: false, }) mockGetIs96ChannelPipetteAttached.mockReturnValue(false) + mockGetShowPipetteCalibrationWarning.mockReturnValue(false) mockUseInstrumentsQuery.mockReturnValue({ data: { data: [] }, } as any) mockPipetteCard.mockReturnValue(
Mock PipetteCard
) mockGripperCard.mockReturnValue(
Mock GripperCard
) mockModuleCard.mockReturnValue(
Mock ModuleCard
) + mockPipetteRecalibrationWarning.mockReturnValue( +
Mock PipetteRecalibrationWarning
+ ) mockUseIsOT3.mockReturnValue(false) }) afterEach(() => { @@ -167,6 +183,12 @@ describe('InstrumentsAndModules', () => { const [{ getByText }] = render() getByText('Mock PipetteCard') }) + it('renders pipette recalibration recommendation banner when offsets fail reasonability checks', () => { + mockGetShowPipetteCalibrationWarning.mockReturnValue(true) + mockUseIsRobotViewable.mockReturnValue(true) + const [{ getByText }] = render() + getByText('Mock PipetteRecalibrationWarning') + }) it('fetches offset calibrations on long poll and pipettes, instruments, and modules on short poll', () => { const { pipette: pipette1 } = mockPipetteOffsetCalibration1 const { pipette: pipette2 } = mockPipetteOffsetCalibration2 From 26c7111bbe058134f423d4b6d1878f4f2b10149d Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:00:40 -0400 Subject: [PATCH 31/79] fix(app, components): center content in instruments and modules cards (#13704) The styling of pipette, instrument, and module cards have diverged significantly and do not align images and text with each other on the device details page. We would benefit from a larger refactoring of these cards for consistency, but this PR introduces a temporary fix to align image and text content on these cards. closes RQA-1488 --- app/src/molecules/InstrumentCard/index.tsx | 9 +- .../organisms/Devices/PipetteCard/index.tsx | 14 +- .../__tests__/GripperCard.test.tsx | 2 +- app/src/organisms/ModuleCard/index.tsx | 2 +- .../instrument-diagram.test.tsx.snap | 148 +++++++++++++++--- .../src/instrument/InstrumentDiagram.tsx | 15 +- 6 files changed, 153 insertions(+), 37 deletions(-) diff --git a/app/src/molecules/InstrumentCard/index.tsx b/app/src/molecules/InstrumentCard/index.tsx index e24870c7610..10ed74c8fa1 100644 --- a/app/src/molecules/InstrumentCard/index.tsx +++ b/app/src/molecules/InstrumentCard/index.tsx @@ -69,8 +69,13 @@ export function InstrumentCard(props: InstrumentCardProps): JSX.Element { {...styleProps} > {isGripperAttached ? ( - - flex gripper + + Flex Gripper ) : null} {instrumentDiagramProps?.pipetteSpecs != null ? ( diff --git a/app/src/organisms/Devices/PipetteCard/index.tsx b/app/src/organisms/Devices/PipetteCard/index.tsx index ff44bcf3589..ece39b2c202 100644 --- a/app/src/organisms/Devices/PipetteCard/index.tsx +++ b/app/src/organisms/Devices/PipetteCard/index.tsx @@ -15,6 +15,7 @@ import { useOnClickOutside, InstrumentDiagram, BORDERS, + ALIGN_CENTER, } from '@opentrons/components' import { isOT3Pipette, @@ -221,20 +222,15 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { )} {!pipetteIsBad && subsystemUpdateData == null && ( <> - + - + {pipetteModelSpecs !== null ? ( ) : null} diff --git a/app/src/organisms/GripperCard/__tests__/GripperCard.test.tsx b/app/src/organisms/GripperCard/__tests__/GripperCard.test.tsx index 2edf2484a56..aa3229b8537 100644 --- a/app/src/organisms/GripperCard/__tests__/GripperCard.test.tsx +++ b/app/src/organisms/GripperCard/__tests__/GripperCard.test.tsx @@ -61,7 +61,7 @@ describe('GripperCard', () => { it('renders correct info when gripper is attached', () => { const { getByText, getByRole } = render(props) - const image = getByRole('img', { name: 'flex gripper' }) + const image = getByRole('img', { name: 'Flex Gripper' }) expect(image.getAttribute('src')).toEqual('flex_gripper.png') getByText('extension mount') getByText('Flex Gripper') diff --git a/app/src/organisms/ModuleCard/index.tsx b/app/src/organisms/ModuleCard/index.tsx index 715ef4b86e9..eb42bf67f39 100644 --- a/app/src/organisms/ModuleCard/index.tsx +++ b/app/src/organisms/ModuleCard/index.tsx @@ -279,7 +279,7 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { onCloseClick={() => setShowTestShake(false)} /> )} - + @@ -17,7 +29,18 @@ exports[`InstrumentDiagram 96-channel GEN1 renders correctly 1`] = ` exports[`InstrumentDiagram Multi-channel GEN2 renders correctly 1`] = ` .c0 { - min-width: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-transform: scaleX(-1); -ms-transform: scaleX(-1); transform: scaleX(-1); @@ -30,6 +53,7 @@ exports[`InstrumentDiagram Multi-channel GEN2 renders correctly 1`] = ` > @@ -37,7 +61,18 @@ exports[`InstrumentDiagram Multi-channel GEN2 renders correctly 1`] = ` exports[`InstrumentDiagram Multi-channel renders correctly 1`] = ` .c0 { - min-width: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; }
@@ -52,7 +88,18 @@ exports[`InstrumentDiagram Multi-channel renders correctly 1`] = ` exports[`InstrumentDiagram Single-channel GEN2 renders correctly 1`] = ` .c0 { - min-width: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-transform: scaleX(-1); -ms-transform: scaleX(-1); transform: scaleX(-1); @@ -65,6 +112,7 @@ exports[`InstrumentDiagram Single-channel GEN2 renders correctly 1`] = ` > @@ -72,7 +120,18 @@ exports[`InstrumentDiagram Single-channel GEN2 renders correctly 1`] = ` exports[`InstrumentDiagram Single-channel renders correctly 1`] = ` .c0 { - min-width: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; }
@@ -87,7 +147,18 @@ exports[`InstrumentDiagram Single-channel renders correctly 1`] = ` exports[`InstrumentDiagram eight-channel FLEX renders correctly 1`] = ` .c0 { - min-width: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-transform: scaleX(-1); -ms-transform: scaleX(-1); transform: scaleX(-1); @@ -100,6 +171,7 @@ exports[`InstrumentDiagram eight-channel FLEX renders correctly 1`] = ` > @@ -107,7 +179,18 @@ exports[`InstrumentDiagram eight-channel FLEX renders correctly 1`] = ` exports[`InstrumentDiagram single-channel FLEX renders correctly 1`] = ` .c0 { - min-width: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-transform: scaleX(-1); -ms-transform: scaleX(-1); transform: scaleX(-1); @@ -120,25 +203,13 @@ exports[`InstrumentDiagram single-channel FLEX renders correctly 1`] = ` > `; exports[`InstrumentGroup Renders correctly 1`] = ` -.c2 { - min-width: 0; -} - -.c3 { - min-width: 0; - -webkit-transform: scaleX(-1); - -ms-transform: scaleX(-1); - transform: scaleX(-1); - -webkit-filter: FlipH; - filter: FlipH; -} - .c0 { display: -webkit-box; display: -webkit-flex; @@ -161,6 +232,41 @@ exports[`InstrumentGroup Renders correctly 1`] = ` flex-direction: column; } +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-transform: scaleX(-1); + -ms-transform: scaleX(-1); + transform: scaleX(-1); + -webkit-filter: FlipH; + filter: FlipH; +} +
@@ -188,6 +294,7 @@ exports[`InstrumentGroup Renders correctly 1`] = ` > @@ -200,6 +307,7 @@ exports[`InstrumentGroup Renders correctly 1`] = ` > diff --git a/components/src/instrument/InstrumentDiagram.tsx b/components/src/instrument/InstrumentDiagram.tsx index b0b682c57fd..3a610d46f57 100644 --- a/components/src/instrument/InstrumentDiagram.tsx +++ b/components/src/instrument/InstrumentDiagram.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { FlattenSimpleInterpolation } from 'styled-components' -import { Box } from '..' +import { Flex } from '../primitives' +import { ALIGN_CENTER, JUSTIFY_CENTER } from '../styles' import singleSrc from '@opentrons/components/src/instrument/single_channel_GEN1_800px.png' import multiSrc from '@opentrons/components/src/instrument/multi-channel_GEN1_800px.png' import singleGEN2Src from '@opentrons/components/src/instrument/single-channel_GEN2_800px.png' @@ -40,13 +41,19 @@ export function InstrumentDiagram(props: InstrumentDiagramProps): JSX.Element { } } return ( - - - + + ) } From 34c82500cad52b838a202a3118e98753c148076b Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:38:46 -0400 Subject: [PATCH 32/79] apply identity vector labware offsets (#13694) Currently, identity vector labware offset data are filtered in the CurrentOffsetsTable component. This unintentionally renders and empty offsets table when there are some offsets saved and they are all equal to the identity vector, rather than the intended "No labware offset data yet" text. closes RQA-1626 closes RQA-1627 --- .../CurrentOffsetsTable.tsx | 12 ++++++------ .../__tests__/CurrentOffsetsTable.test.tsx | 9 +-------- .../ProtocolRun/SetupLabwarePositionCheck/index.tsx | 7 +++++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx index 48bfbd2fe44..c78d3b0b154 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx @@ -22,7 +22,6 @@ import { StyledText } from '../../../../atoms/text' import { LabwareOffsetTabs } from '../../../LabwareOffsetTabs' import { OffsetVector } from '../../../../molecules/OffsetVector' import { PythonLabwareOffsetSnippet } from '../../../../molecules/PythonLabwareOffsetSnippet' -import { getLatestCurrentOffsets } from './utils' import type { LabwareOffset } from '@opentrons/api-client' import type { RunTimeCommand, @@ -71,7 +70,6 @@ export function CurrentOffsetsTable( const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn ) - const latestCurrentOffsets = getLatestCurrentOffsets(currentOffsets) const TableComponent = ( @@ -83,7 +81,7 @@ export function CurrentOffsetsTable( - {latestCurrentOffsets.map(offset => { + {currentOffsets.map(offset => { const labwareDisplayName = offset.definitionUri in defsByURI ? getLabwareDisplayName(defsByURI[offset.definitionUri]) @@ -112,7 +110,7 @@ export function CurrentOffsetsTable( const JupyterSnippet = ( + labwareOffsets={currentOffsets.map(o => pick(o, ['definitionUri', 'location', 'vector']) )} commands={commands ?? []} @@ -123,7 +121,7 @@ export function CurrentOffsetsTable( const CommandLineSnippet = ( + labwareOffsets={currentOffsets.map(o => pick(o, ['definitionUri', 'location', 'vector']) )} commands={commands ?? []} @@ -137,7 +135,9 @@ export function CurrentOffsetsTable( justifyContent={JUSTIFY_SPACE_BETWEEN} padding={SPACING.spacing16} > - {t('applied_offset_data')} + + {i18n.format(t('applied_offset_data'), 'upperCase')} + {isLabwareOffsetCodeSnippetsOn ? ( { }) it('renders the correct text', () => { const { getByText } = render(props) - getByText('Applied Labware Offset data') + getByText('APPLIED LABWARE OFFSET DATA') getByText('location') getByText('labware') getByText('labware offset data') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index 7df13a85fcf..f81a4f7bbae 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -22,6 +22,7 @@ import { CurrentOffsetsTable } from './CurrentOffsetsTable' import { useLaunchLPC } from '../../../LabwarePositionCheck/useLaunchLPC' import { StyledText } from '../../../../atoms/text' import type { LabwareOffset } from '@opentrons/api-client' +import { getLatestCurrentOffsets } from './utils' interface SetupLabwarePositionCheckProps { expandLabwareStep: () => void @@ -74,15 +75,17 @@ export function SetupLabwarePositionCheck( const { launchLPC, LPCWizard } = useLaunchLPC(runId, protocolName) + const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets) + return ( - {sortedOffsets.length > 0 ? ( + {nonIdentityOffsets.length > 0 ? ( Date: Thu, 5 Oct 2023 15:47:53 -0400 Subject: [PATCH 33/79] fix(app, app-shell): Add app layer robot update flow error handling (#13717) Closes RQA-1562, RQA-1714, RQA-1715, RQA-1680, RQA-1536, RQA-1747 * refactor(app, app-shell): add manual file upload state to redux store * refactor(app): safely start robot update Adds a wrapper to clear existing robot update session before starting a new one. * fix(app): add robot update error handling Add guarded transitions between update stages and refactors robot update flows. * fix(app): increase robot update restart timeout interval OT-2 restarts can take longer than the timeout interval suggests. --- app-shell-odd/src/system-update/index.ts | 6 +- app-shell/src/robot-update/index.ts | 6 +- .../assets/localization/en/app_settings.json | 8 +- .../localization/en/device_settings.json | 113 ++--- app/src/organisms/Devices/RobotOverview.tsx | 7 +- .../Devices/RobotOverviewOverflowMenu.tsx | 4 +- .../AdvancedTab/RobotServerVersion.tsx | 15 +- .../AdvancedTab/UpdateRobotSoftware.tsx | 14 +- .../RobotSettings/RobotSettingsAdvanced.tsx | 4 +- .../RobotUpdateProgressModal.tsx | 386 +++++++++++------- .../UpdateBuildroot/UpdateRobotModal.tsx | 5 +- .../UpdateBuildroot/ViewUpdateModal.tsx | 31 +- .../RobotUpdateProgressModal.test.tsx | 94 +++-- .../__tests__/UpdateBuildroot.test.tsx | 7 +- .../__tests__/ViewUpdateModal.test.tsx | 65 +-- .../__tests__/useRobotUpdateInfo.test.tsx | 95 +++++ .../RobotSettings/UpdateBuildroot/index.tsx | 80 ++-- .../UpdateBuildroot/useRobotUpdateInfo.ts | 147 +++++++ app/src/organisms/UpdateAppModal/index.tsx | 17 +- app/src/organisms/UpdateRobotBanner/index.tsx | 16 +- .../robot-admin/__tests__/selectors.test.ts | 7 +- app/src/redux/robot-admin/constants.ts | 4 +- .../robot-update/__tests__/hooks.test.ts | 35 ++ .../robot-update/__tests__/selectors.test.ts | 11 + app/src/redux/robot-update/hooks.ts | 22 + app/src/redux/robot-update/selectors.ts | 4 + app/src/redux/robot-update/types.ts | 1 + 27 files changed, 797 insertions(+), 407 deletions(-) create mode 100644 app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx create mode 100644 app/src/organisms/Devices/RobotSettings/UpdateBuildroot/useRobotUpdateInfo.ts create mode 100644 app/src/redux/robot-update/__tests__/hooks.test.ts create mode 100644 app/src/redux/robot-update/hooks.ts diff --git a/app-shell-odd/src/system-update/index.ts b/app-shell-odd/src/system-update/index.ts index d7d3aa7660d..5359707b6dd 100644 --- a/app-shell-odd/src/system-update/index.ts +++ b/app-shell-odd/src/system-update/index.ts @@ -29,7 +29,8 @@ let updateSet: ReleaseSetFilepaths | null = null const readFileInfoAndDispatch = ( dispatch: Dispatch, - fileName: string + fileName: string, + isManualFile: boolean = false ): Promise => readUserFileInfo(fileName) .then(fileInfo => ({ @@ -37,6 +38,7 @@ const readFileInfoAndDispatch = ( payload: { systemFile: fileInfo.systemFile, version: fileInfo.versionInfo.opentrons_api_version, + isManualFile, }, })) .catch((error: Error) => ({ @@ -99,7 +101,7 @@ export function registerRobotSystemUpdate(dispatch: Dispatch): Dispatch { case 'robotUpdate:READ_USER_FILE': { const { systemFile } = action.payload as { systemFile: string } // eslint-disable-next-line @typescript-eslint/no-floating-promises - readFileInfoAndDispatch(dispatch, systemFile) + readFileInfoAndDispatch(dispatch, systemFile, true) break } case 'robotUpdate:READ_SYSTEM_FILE': { diff --git a/app-shell/src/robot-update/index.ts b/app-shell/src/robot-update/index.ts index cc222fa1ef0..5ea9f9307eb 100644 --- a/app-shell/src/robot-update/index.ts +++ b/app-shell/src/robot-update/index.ts @@ -41,7 +41,8 @@ const updateSet: Record = { const readFileAndDispatchInfo = ( dispatch: Dispatch, - filename: string + filename: string, + isManualFile: boolean = false ): Promise => readUpdateFileInfo(filename) .then(fileInfo => ({ @@ -49,6 +50,7 @@ const readFileAndDispatchInfo = ( payload: { systemFile: fileInfo.systemFile, version: fileInfo.versionInfo.opentrons_api_version, + isManualFile, }, })) .catch((error: Error) => ({ @@ -130,7 +132,7 @@ export function registerRobotUpdate(dispatch: Dispatch): Dispatch { case 'robotUpdate:READ_USER_FILE': { const { systemFile } = action.payload as { systemFile: string } - readFileAndDispatchInfo(dispatch, systemFile) + readFileAndDispatchInfo(dispatch, systemFile, true) break } diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index c77e2e4012d..18fa61d9126 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -58,8 +58,8 @@ "opentrons_app_update_available_variation": "An Opentrons App update is available.", "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", "opentrons_cares_about_privacy": "Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.", - "opt_in_description": "Automatically send us anonymous diagnostics and usage data. We only use this information to improve our products.", "opt_in": "Opt in", + "opt_in_description": "Automatically send us anonymous diagnostics and usage data. We only use this information to improve our products.", "opt_out": "Opt out", "ot2_advanced_settings": "OT-2 Advanced Settings", "override_path": "override path", @@ -68,6 +68,7 @@ "prevent_robot_caching_description": "The app will immediately clear unavailable robots and will not remember unavailable robots while this is enabled. On networks with many robots, preventing caching may improve network performance at the expense of slower and less reliable robot discovery on app launch.", "previous_releases": "View previous Opentrons releases", "privacy": "Privacy", + "problem_during_update": "This update is taking longer than usual.", "prompt": "Always show the prompt to choose calibration block or trash bin", "receive_alert": "Receive an alert when an Opentrons software update is available.", "remind_later": "Remind me later", @@ -80,10 +81,10 @@ "setup_connection": "Set up connection", "share_app_analytics": "Share App Analytics with Opentrons", "share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", - "share_display_usage_description": "Data on how you interact with the touchscreen on Flex.", "share_display_usage": "Share display usage", - "share_robot_logs_description": "Data on actions the robot does, like running protocols.", + "share_display_usage_description": "Data on how you interact with the touchscreen on Flex.", "share_robot_logs": "Share robot logs", + "share_robot_logs_description": "Data on actions the robot does, like running protocols.", "show_labware_offset_snippets": "Show Labware Offset data code snippets", "show_labware_offset_snippets_description": "Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "software_update_available": "Software Update Available", @@ -91,6 +92,7 @@ "successfully_deleted_unavail_robots": "Successfully deleted unavailable robots", "tip_length_cal_method": "Tip Length Calibration Method", "trash_bin": "Always use trash bin to calibrate", + "try_restarting_the_update": "Try restarting the update.", "turn_off_updates": "Turn off software update notifications in App Settings.", "up_to_date": "Up to date", "update_alerts": "Software Update Alerts", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 554f1c2aadc..6f16915ac34 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -1,12 +1,12 @@ { "about_advanced": "About", - "about_calibration_description_ot3": "For the robot to move accurately and precisely, you need to calibrate it. Pipette and gripper calibration is an automated process that uses a calibration probe or pin.After calibration is complete, you can save the calibration data to your computer as a JSON file.", "about_calibration_description": "For the robot to move accurately and precisely, you need to calibrate it. Positional calibration happens in three parts: deck calibration, pipette offset calibration and tip length calibration.", + "about_calibration_description_ot3": "For the robot to move accurately and precisely, you need to calibrate it. Pipette and gripper calibration is an automated process that uses a calibration probe or pin.After calibration is complete, you can save the calibration data to your computer as a JSON file.", "about_calibration_title": "About Calibration", "advanced": "Advanced", "alpha_description": "Warning: alpha releases are feature-complete but may contain significant bugs.", - "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", "alternative_security_types": "Alternative security types", + "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", "app_change_in": "App Changes in {{version}}", "apply_historic_offsets": "Apply Labware Offsets", "are_you_sure_you_want_to_disconnect": "Are you sure you want to disconnect from {{ssid}}?", @@ -14,54 +14,54 @@ "boot_scripts": "Boot scripts", "browse_file_system": "Browse file system", "bug_fixes": "Bug Fixes", + "calibrate_deck": "Calibrate deck", "calibrate_deck_description": "For pre-2019 robots that do not have crosses etched on the deck.", "calibrate_deck_to_dots": "Calibrate deck to dots", - "calibrate_deck": "Calibrate deck", "calibrate_gripper": "Calibrate gripper", "calibrate_module": "Calibrate module", "calibrate_now": "Calibrate now", "calibrate_pipette": "Calibrate Pipette Offset", + "calibration": "Calibration", "calibration_health_check_description": "Check the accuracy of key calibration points without recalibrating the robot.", "calibration_health_check_title": "Calibration Health Check", - "calibration": "Calibration", "change_network": "Change network", "characters_max": "17 characters max", "check_for_updates": "Check for updates", "checking_for_updates": "Checking for updates", + "choose": "Choose...", "choose_network_type": "Choose network type", "choose_reset_settings": "Choose reset settings", - "choose": "Choose...", "clear_all_data": "Clear all data", - "clear_all_stored_data_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.", "clear_all_stored_data": "Clear all stored data", + "clear_all_stored_data_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.", "clear_calibration_data": "Clear calibration data", "clear_data_and_restart_robot": "Clear data and restart robot", "clear_individual_data": "Clear individual data", "clear_option_authorized_keys": "Clear SSH public keys", - "clear_option_boot_scripts_description": "Clears scripts that modify the robot's behavior when powered on.", "clear_option_boot_scripts": "Clear custom boot scripts", + "clear_option_boot_scripts_description": "Clears scripts that modify the robot's behavior when powered on.", "clear_option_deck_calibration": "Clear deck calibration", "clear_option_gripper_calibration": "Clear gripper calibration", "clear_option_gripper_offset_calibrations": "Clear gripper calibration", "clear_option_module_calibration": "Clear module calibration", "clear_option_pipette_calibrations": "Clear pipette calibration", "clear_option_pipette_offset_calibrations": "Clear pipette offset calibrations", - "clear_option_runs_history_subtext": "Clears information about past runs of all protocols.", "clear_option_runs_history": "Clear protocol run history", + "clear_option_runs_history_subtext": "Clears information about past runs of all protocols.", "clear_option_tip_length_calibrations": "Clear tip length calibrations", "confirm_device_reset_description": "This will permanently delete all protocol, calibration, and other data. You’ll have to redo initial setup before using the robot again.", "confirm_device_reset_heading": "Are you sure you want to reset your device?", + "connect": "Connect", "connect_the_estop_to_continue": "Connect the E-stop to continue", "connect_to_wifi_network": "Connect to Wi-Fi network", + "connect_via": "Connect via {{type}}", "connect_via_usb_description_1": "1. Connect the USB A-to-B cable to the robot’s USB-B port.", "connect_via_usb_description_2": "2. Connect the cable to an open USB port on your computer.", "connect_via_usb_description_3": "3. Launch the Opentrons App on the computer to continue.", - "connect_via": "Connect via {{type}}", - "connect": "Connect", + "connected": "Connected", "connected_network": "Connected Network", "connected_to_ssid": "Connected to {{ssid}}", "connected_via": "Connected via {{networkInterface}}", - "connected": "Connected", "connecting_to": "Connecting to {{ssid}}...", "connection_description_ethernet": "Connect to your lab's wired network.", "connection_description_usb": "Connect directly to a computer (running the Opentrons App).", @@ -69,60 +69,60 @@ "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", "connection_to_robot_lost": "Connection to robot lost", "deck_calibration_description": "Calibrating the deck is required for new robots or after you relocate your robot. Recalibrating the deck will require you to also recalibrate pipette offsets.", - "deck_calibration_missing_no_pipette": "Deck calibration missing. Attach a pipette to perform deck calibration.", "deck_calibration_missing": "Deck calibration missing", + "deck_calibration_missing_no_pipette": "Deck calibration missing. Attach a pipette to perform deck calibration.", "deck_calibration_modal_description": "Calibrating pipette offset before deck calibration when both are needed isn’t suggested. Calibrating the deck clears all other calibration data. ", "deck_calibration_modal_pipette_description": "Would you like to continue with pipette offset calibration?", "deck_calibration_modal_title": "Are you sure you want to calibrate?", "deck_calibration_recommended": "Deck calibration recommended", "deck_calibration_title": "Deck Calibration", "dev_tools_description": "Access additional logging and feature flags.", + "device_reset": "Device Reset", "device_reset_description": "Reset labware calibration, boot scripts, and/or robot calibration to factory settings.", "device_reset_slideout_description": "Select individual settings to only clear specific data types.", - "device_reset": "Device Reset", "device_resets_cannot_be_undone": "Resets cannot be undone", "directly_connected_to_this_computer": "Directly connected to this computer.", + "disconnect": "Disconnect", "disconnect_from_ssid": "Disconnect from {{ssid}}", + "disconnect_from_wifi": "Disconnect from Wi-Fi", "disconnect_from_wifi_network_failure": "Your robot was unable to disconnect from Wi-Fi network {{ssid}}.", "disconnect_from_wifi_network_success": "Your robot has successfully disconnected from the Wi-Fi network.", - "disconnect_from_wifi": "Disconnect from Wi-Fi", - "disconnect": "Disconnect", "disconnected_from_wifi": "Disconnected from Wi-Fi", "disconnecting_from_wifi_network": "Disconnecting from Wi-Fi network {{ssid}}", "disengaged": "Disengaged", "display_brightness": "Display Brightness", - "display_led_lights_description": "Control the strip of color lights on the front of the robot.", "display_led_lights": "Status LEDs", + "display_led_lights_description": "Control the strip of color lights on the front of the robot.", "display_sleep_settings": "Display Sleep Settings", - "do_not_turn_off": "Do not turn off the robot while updating", + "do_not_turn_off": "This could take up to 15 minutes. Don't turn off the robot.", "done": "Done", + "download": "Download", "download_calibration_data": "Download calibration logs", "download_error": "Download error", "download_logs": "Download logs", - "download": "Download", "downloading_logs": "Downloading logs...", "downloading_software": "Downloading software...", "downloading_update": "Downloading update...", "e_stop_connected": "E-stop successfully connected", "e_stop_not_connected": "Connect the E-stop to an auxiliary port on the back of the robot.", - "enable_status_light_description": "Turn on or off the strip of color LEDs on the front of the robot.", "enable_status_light": "Enable status light", + "enable_status_light_description": "Turn on or off the strip of color LEDs on the front of the robot.", "engaged": "Engaged", "enter_network_name": "Enter network name", "enter_password": "Enter password", + "estop": "E-stop", "estop_disengaged": "E-stop Disengaged", "estop_engaged": "E-stop Engaged", - "estop_missing_description": "Your E-stop could be damaged or detached. {{robotName}} lost its connection to the E-stop, so it canceled the protocol. Connect a functioning E-stop to continue.", "estop_missing": "E-stop missing", - "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have Flex move the gantry to its home position.", + "estop_missing_description": "Your E-stop could be damaged or detached. {{robotName}} lost its connection to the E-stop, so it canceled the protocol. Connect a functioning E-stop to continue.", "estop_pressed": "E-stop pressed", - "estop": "E-stop", - "ethernet_connection_description": "Connect an Ethernet cable to the back of the robot and a network switch or hub.", + "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have Flex move the gantry to its home position.", "ethernet": "Ethernet", + "ethernet_connection_description": "Connect an Ethernet cable to the back of the robot and a network switch or hub.", "exit": "exit", + "factory_reset": "Factory Reset", "factory_reset_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.", "factory_reset_modal_description": "This data cannot be retrieved later.", - "factory_reset": "Factory Reset", "factory_resets_cannot_be_undone": "Factory resets cannot be undone.", "failed_to_connect_to_ssid": "Failed to connect to {{ssid}}", "feature_flags": "Feature Flags", @@ -130,8 +130,8 @@ "finish_setup": "Finish setup", "firmware_version": "Firmware Version", "fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health", - "gantry_homing_description": "Homes the gantry along the z-axis.", "gantry_homing": "Home Gantry on Restart", + "gantry_homing_description": "Homes the gantry along the z-axis.", "go_to_advanced_settings": "Go to Advanced App Settings", "gripper_calibration_description": "Gripper calibration uses a metal pin to determine the gripper's exact position relative to precision-cut squares on deck slots.", "gripper_calibration_title": "Gripper Calibration", @@ -144,30 +144,30 @@ "installing_software": "Installing software...", "installing_update": "Installing update...", "ip_address": "IP Address", - "join_other_network_error_message": "Must be 2–32 characters long", "join_other_network": "Join other network", + "join_other_network_error_message": "Must be 2–32 characters long", + "jupyter_notebook": "Jupyter Notebook", "jupyter_notebook_description": "Open the Jupyter Notebook running on this robot in the web browser. This is an experimental feature.", "jupyter_notebook_link": "Learn more about using Jupyter notebook", - "jupyter_notebook": "Jupyter Notebook", - "last_calibrated_label": "Last Calibrated", "last_calibrated": "Last calibrated: {{date}}", + "last_calibrated_label": "Last Calibrated", "launch_jupyter_notebook": "Launch Jupyter Notebook", "legacy_settings": "Legacy Settings", "mac_address": "MAC Address", "minutes": "{{minute}} minutes", "missing_calibration": "Missing calibration", "model_and_serial": "Pipette Model and Serial", - "module_calibration_description": "Module calibration uses a pipette and attached probe to determine the module's exact position relative to the deck.", - "module_calibration": "Module Calibration", "module": "Module", + "module_calibration": "Module Calibration", + "module_calibration_description": "Module calibration uses a pipette and attached probe to determine the module's exact position relative to the deck.", "mount": "Mount", "name_love_it": "{{name}}, love it!", "name_rule_description": "Enter up to 17 characters (letters and numbers only)", "name_rule_error_exist": "Oops! Name is already in use. Choose a different name.", "name_rule_error_name_length": "Oops! Robot name must follow the character count and limitations.", "name_rule_error_too_short": "Oops! Too short. Robot name must be at least 1 character.", - "name_your_robot_description": "Don’t worry, you can always change this in your settings.", "name_your_robot": "Name your robot", + "name_your_robot_description": "Don’t worry, you can always change this in your settings.", "need_another_security_type": "Need another security type?", "network_name": "Network Name", "network_settings": "Network Settings", @@ -182,28 +182,29 @@ "no_network_found": "No network found", "no_pipette_attached": "No pipette attached", "none_description": "Not recommended", - "not_calibrated_short": "Not calibrated", "not_calibrated": "Not calibrated yet", + "not_calibrated_short": "Not calibrated", + "not_connected": "Not connected", "not_connected_via_ethernet": "Not connected via Ethernet", "not_connected_via_usb": "Not connected via USB", "not_connected_via_wifi": "Not connected via Wi-Fi", "not_connected_via_wired_usb": "Not connected via wired USB", - "not_connected": "Not connected", "not_now": "Not now", "one_hour": "1 hour", "other_networks": "Other Networks", - "password_error_message": "Must be at least 8 characters", "password": "Password", - "pause_protocol_description": "When enabled, opening the robot door during a run will pause the robot after it has completed its current motion.", + "password_error_message": "Must be at least 8 characters", "pause_protocol": "Pause protocol when robot door opens", + "pause_protocol_description": "When enabled, opening the robot door during a run will pause the robot after it has completed its current motion.", "pipette_calibrations_description": "Pipette calibration uses a metal probe to determine the pipette's exact position relative to precision-cut squares on deck slots.", "pipette_calibrations_title": "Pipette Calibrations", + "pipette_offset_calibration": "pipette offset calibration", "pipette_offset_calibration_missing": "Pipette Offset calibration missing", "pipette_offset_calibration_recommended": "Pipette Offset calibration recommended", - "pipette_offset_calibration": "pipette offset calibration", "pipette_offset_calibrations_history": "See all Pipette Offset Calibration history", "pipette_offset_calibrations_title": "Pipette Offset Calibrations", "privacy": "Privacy", + "problem_during_update": "This update is taking longer than usual.", "proceed_without_updating": "Proceed without update", "protocol_run_history": "Protocol run History", "recalibrate_deck": "Recalibrate deck", @@ -215,16 +216,17 @@ "recalibration_recommended": "Recalibration recommended", "reinstall": "reinstall", "remind_me_later": "Remind me later", + "rename_robot": "Rename robot", "rename_robot_input_error": "Oops! Robot name must follow the character count and limitations.", "rename_robot_input_limitation_detail": "Please enter 17 characters max using valid inputs: letters and numbers.", "rename_robot_prefer_usb_connection": "To ensure reliable renaming of your robot, please connect to it via USB.", "rename_robot_title": "Rename Robot", - "rename_robot": "Rename robot", "requires_restarting_the_robot": "Updating the robot’s software requires restarting the robot", "reset_to_factory_settings": "Reset to factory settings?", "resets_cannot_be_undone": "Resets cannot be undone", "restart_now": "Restart now?", "restart_robot_confirmation_description": "It will take a few minutes for {{robotName}} to restart.", + "restart_taking_too_long": "{{robotName}} restart is taking longer than expected. If the robot appears unresponsive, try restarting the robot manually.", "restarting_robot": "Install complete, robot restarting...", "resume_robot_operations": "Resume robot operations", "returns_your_device_to_new_state": "This returns your device to a new state.", @@ -233,21 +235,21 @@ "robot_name": "Robot Name", "robot_operating_update_available": "Robot Operating System Update Available", "robot_serial_number": "Robot Serial Number", - "robot_server_version_ot3_description": "The Opentrons Flex software includes the robot server and the touchscreen display interface.", "robot_server_version": "Robot Server Version", - "robot_settings_advanced_unknown": "Unknown", + "robot_server_version_ot3_description": "The Opentrons Flex software includes the robot server and the touchscreen display interface.", "robot_settings": "Robot Settings", + "robot_settings_advanced_unknown": "Unknown", "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", "robot_successfully_connected": "Robot successfully connected to {{networkName}}.", - "robot_system_version_available": "Robot System Version {{releaseVersion}} available", "robot_system_version": "Robot System Version", - "robot_up_to_date_description": "It looks like your robot is already up to date, but if you're experiencing issues you can re-apply the latest update.", + "robot_system_version_available": "Robot System Version {{releaseVersion}} available", "robot_up_to_date": "Robot is up to date", + "robot_up_to_date_description": "It looks like your robot is already up to date, but if you're experiencing issues you can re-apply the latest update.", "robot_update_available": "Robot Update Available", "robot_update_success": "Robot software successfully updated", "search_again": "Search again", - "searching_for_networks": "Searching for networks...", "searching": "Searching", + "searching_for_networks": "Searching for networks...", "security_type": "Security Type", "select_a_network": "Select a network", "select_a_security_type": "Select a security type", @@ -255,26 +257,26 @@ "select_authentication_method": "Select authentication method for your selected network.", "sending_software": "Sending software...", "serial": "Serial", - "share_logs_with_opentrons_description_short": "Share anonymous robot logs with Opentrons.", + "share_logs_with_opentrons": "Share Robot logs with Opentrons", "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", + "share_logs_with_opentrons_description_short": "Share anonymous robot logs with Opentrons.", "share_logs_with_opentrons_short": "Share Robot logs", - "share_logs_with_opentrons": "Share Robot logs with Opentrons", - "short_trash_bin_description": "For pre-2019 robots with trash bins that are 55mm tall (instead of 77mm default)", "short_trash_bin": "Short trash bin", - "show_password": "Show Password", + "short_trash_bin_description": "For pre-2019 robots with trash bins that are 55mm tall (instead of 77mm default)", "show": "Show", + "show_password": "Show Password", "sign_into_wifi": "Sign into Wi-Fi", "software_is_up_to_date": "Your software is already up to date!", "software_update_error": "Software update error", "some_robot_controls_are_not_available": "Some robot controls are not available when run is in progress", "ssh_public_keys": "SSH public keys", "subnet_mask": "Subnet Mask", - "successfully_connected_to_network": "Successfully connected to {{ssid}}!", "successfully_connected": "Successfully connected!", + "successfully_connected_to_network": "Successfully connected to {{ssid}}!", "supported_protocol_api_versions": "Supported Protocol API Versions", "switch_to_usb_description": "If your network uses a different authentication method, connect to the Opentrons App and finish Wi-Fi setup there.", - "text_size_description": "Text on all screens will adjust to the size you choose below.", "text_size": "Text Size", + "text_size_description": "Text on all screens will adjust to the size you choose below.", "tip_length_calibrations_history": "See all Tip Length Calibration history", "tip_length_calibrations_title": "Tip Length Calibrations", "tiprack": "Tip Rack", @@ -282,24 +284,25 @@ "touchscreen_sleep": "Touchscreen Sleep", "troubleshooting": "Troubleshooting", "try_again": "Try again", + "try_restarting_the_update": "Try restarting the update.", "up_to_date": "up to date", "update_available": "Update Available", "update_channel_description": "Stable receives the latest stable releases. Beta allows you to try out new in-progress features before they launch in Stable channel, but they have not completed testing yet.", "update_complete": "Update complete!", "update_found": "Update found!", "update_robot_now": "Update robot now", + "update_robot_software": "Update robot software manually with a local file (.zip)", "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", "update_robot_software_link": "Launch Opentrons software update page", - "update_robot_software": "Update robot software manually with a local file (.zip)", - "updating_robot_system": "Updating the robot software requires restarting the robot", "updating": "Updating", + "updating_robot_system": "Updating the robot software requires restarting the robot", "usage_settings": "Usage Settings", - "usb_to_ethernet_description": "Looking for USB-to-Ethernet Adapter info?", "usb": "USB", - "use_older_aspirate_description": "Aspirate with the less accurate volumetric calibrations that were used before version 3.7.0. Use this if you need consistency with pre-v3.7.0 results. This only affects GEN1 P10S, P10M, P50M, and P300S pipettes.", + "usb_to_ethernet_description": "Looking for USB-to-Ethernet Adapter info?", "use_older_aspirate": "Use older aspirate behavior", - "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Opentrons Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", + "use_older_aspirate_description": "Aspirate with the less accurate volumetric calibrations that were used before version 3.7.0. Use this if you need consistency with pre-v3.7.0 results. This only affects GEN1 P10S, P10M, P50M, and P300S pipettes.", "use_older_protocol_analysis_method": "Use older protocol analysis method", + "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Opentrons Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", "validating_software": "Validating software...", "view_details": "View details", "view_latest_release_notes_at": "View latest release notes at {{url}}", @@ -314,13 +317,13 @@ "wired_ip": "Wired IP", "wired_mac_address": "Wired MAC Address", "wired_subnet_mask": "Wired Subnet Mask", - "wired_usb_description": "Learn about connecting to a robot via USB", "wired_usb": "Wired USB", + "wired_usb_description": "Learn about connecting to a robot via USB", "wireless_ip": "Wireless IP", "wireless_mac_address": "Wireless MAC Address", "wireless_subnet_mask": "Wireless Subnet Mask", - "wpa2_personal_description": "Most labs use this method", "wpa2_personal": "WPA2 Personal", + "wpa2_personal_description": "Most labs use this method", "yes_clear_data_and_restart_robot": "Yes, clear data and restart robot", "your_mac_address_is": "Your MAC Address is {{macAddress}}", "your_robot_is_ready_to_go": "Your robot is ready to go." diff --git a/app/src/organisms/Devices/RobotOverview.tsx b/app/src/organisms/Devices/RobotOverview.tsx index 483441f7d01..0b03bb8a43e 100644 --- a/app/src/organisms/Devices/RobotOverview.tsx +++ b/app/src/organisms/Devices/RobotOverview.tsx @@ -112,12 +112,7 @@ export function RobotOverview({ - {robot != null ? ( - - ) : null} + - {showSoftwareUpdateModal && - robot != null && - robot.status !== UNREACHABLE ? ( + {showSoftwareUpdateModal ? ( setShowSoftwareUpdateModal(false)} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx index 111a73d60c4..f3806a8ed37 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx @@ -13,9 +13,8 @@ import { JUSTIFY_FLEX_END, } from '@opentrons/components' import { StyledText } from '../../../../atoms/text' -import { Portal } from '../../../../App/portal' import { TertiaryButton } from '../../../../atoms/buttons' -import { getRobotApiVersion, UNREACHABLE } from '../../../../redux/discovery' +import { getRobotApiVersion } from '../../../../redux/discovery' import { getRobotUpdateDisplayInfo } from '../../../../redux/robot-update' import { UpdateRobotBanner } from '../../../UpdateRobotBanner' import { useIsOT3, useRobot } from '../../hooks' @@ -46,13 +45,11 @@ export function RobotServerVersion({ return ( <> - {showVersionInfoModal && robot != null && robot.status !== UNREACHABLE ? ( - - setShowVersionInfoModal(false)} - /> - + {showVersionInfoModal ? ( + setShowVersionInfoModal(false)} + /> ) : null} {autoUpdateAction !== 'reinstall' && robot != null ? ( diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx index 700bcbeb91a..980d4c4f791 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useSelector, useDispatch } from 'react-redux' +import { useSelector } from 'react-redux' import { css } from 'styled-components' import { @@ -18,12 +18,10 @@ import { StyledText } from '../../../../atoms/text' import { ExternalLink } from '../../../../atoms/Link/ExternalLink' import { TertiaryButton } from '../../../../atoms/buttons' import { Tooltip } from '../../../../atoms/Tooltip' -import { - getRobotUpdateDisplayInfo, - startRobotUpdate, -} from '../../../../redux/robot-update' +import { getRobotUpdateDisplayInfo } from '../../../../redux/robot-update' +import { useDispatchStartRobotUpdate } from '../../../../redux/robot-update/hooks' -import type { State, Dispatch } from '../../../../redux/types' +import type { State } from '../../../../redux/types' const OT_APP_UPDATE_PAGE_LINK = 'https://opentrons.com/ot-app/' const HIDDEN_CSS = css` @@ -43,18 +41,18 @@ export function UpdateRobotSoftware({ isRobotBusy, }: UpdateRobotSoftwareProps): JSX.Element { const { t } = useTranslation('device_settings') - const dispatch = useDispatch() const { updateFromFileDisabledReason } = useSelector((state: State) => { return getRobotUpdateDisplayInfo(state, robotName) }) const updateDisabled = updateFromFileDisabledReason !== null const [updateButtonProps, updateButtonTooltipProps] = useHoverTooltip() const inputRef = React.useRef(null) + const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() const handleChange: React.ChangeEventHandler = event => { const { files } = event.target if (files?.length === 1 && !updateDisabled) { - dispatch(startRobotUpdate(robotName, files[0].path)) + dispatchStartRobotUpdate(robotName, files[0].path) onUpdateStart() } } diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx index 7a744d99dae..06f6b3dc0ec 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -124,9 +124,7 @@ export function RobotSettingsAdvanced({ return ( <> - {showSoftwareUpdateModal && - robot != null && - robot.status !== UNREACHABLE ? ( + {showSoftwareUpdateModal ? ( setShowSoftwareUpdateModal(false)} diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx index 31ca9616b58..c815dcf3ab3 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx @@ -1,11 +1,12 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { css } from 'styled-components' import { Flex, Icon, + Link, NewPrimaryBtn, NewSecondaryBtn, JUSTIFY_FLEX_END, @@ -22,86 +23,182 @@ import { LegacyModal } from '../../../../molecules/LegacyModal' import { ProgressBar } from '../../../../atoms/ProgressBar' import { FOOTER_BUTTON_STYLE } from './UpdateRobotModal' import { - clearRobotUpdateSession, startRobotUpdate, + clearRobotUpdateSession, + getRobotSessionIsManualFile, } from '../../../../redux/robot-update' +import { useDispatchStartRobotUpdate } from '../../../../redux/robot-update/hooks' +import { useRobotUpdateInfo } from './useRobotUpdateInfo' import successIcon from '../../../../assets/images/icon_success.png' +import type { State } from '../../../../redux/types' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/incidental' -import type { Dispatch } from '../../../../redux/types' -import type { UpdateStep } from '.' -import type { RobotUpdateAction } from '../../../../redux/robot-update/types' +import type { RobotUpdateSession } from '../../../../redux/robot-update/types' +import type { UpdateStep } from './useRobotUpdateInfo' -interface SuccessOrErrorProps { - errorMessage?: string | null +const UPDATE_PROGRESS_BAR_STYLE = css` + margin-top: ${SPACING.spacing24}; + margin-bottom: ${SPACING.spacing24}; + border-radius: ${BORDERS.borderRadiusSize3}; + background: ${COLORS.medGreyEnabled}; + width: 17.12rem; +` +const UPDATE_TEXT_STYLE = css` + color: ${COLORS.darkGreyEnabled}; + font-size: 0.8rem; +` +const TRY_RESTART_STYLE = css` + color: ${COLORS.blueEnabled}; + font-size: 0.8rem; +` +const HIDDEN_CSS = css` + position: fixed; + clip: rect(1px 1px 1px 1px); +` + +interface RobotUpdateProgressModalProps { + robotName: string + session: RobotUpdateSession | null + closeUpdateBuildroot?: () => void } -function SuccessOrError({ errorMessage }: SuccessOrErrorProps): JSX.Element { +export function RobotUpdateProgressModal({ + robotName, + session, + closeUpdateBuildroot, +}: RobotUpdateProgressModalProps): JSX.Element { + const dispatch = useDispatch() const { t } = useTranslation('device_settings') - const IMAGE_ALT = 'Welcome screen background image' - let renderedImg: JSX.Element - if (!errorMessage) - renderedImg = ( - {IMAGE_ALT} - ) - else - renderedImg = ( - - ) + const [showFileSelect, setShowFileSelect] = React.useState(false) + const installFromFileRef = React.useRef(null) + const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() + const manualFileUsedForUpdate = useSelector((state: State) => + getRobotSessionIsManualFile(state) + ) + const completeRobotUpdateHandler = (): void => { + if (closeUpdateBuildroot != null) closeUpdateBuildroot() + } + const reinstallUpdate = React.useCallback(() => { + dispatchStartRobotUpdate(robotName) + }, [robotName]) + + const { error } = session || { error: null } + const { updateStep, progressPercent } = useRobotUpdateInfo(session) + useStatusBarAnimation(error != null) + useCleanupRobotUpdateSessionOnDismount() + + const handleFileSelect: React.ChangeEventHandler = event => { + const { files } = event.target + if (files?.length === 1) { + dispatch(startRobotUpdate(robotName, files[0].path)) + } + setShowFileSelect(false) + } + React.useEffect(() => { + if (showFileSelect && installFromFileRef.current) + installFromFileRef.current.click() + }, [showFileSelect]) + + const hasStoppedUpdating = error || updateStep === 'finished' + const letUserExitUpdate = useAllowExitIfUpdateStalled( + updateStep, + progressPercent + ) + + let modalBodyText = t('installing_update') + let subProgressBarText = t('do_not_turn_off') + if (updateStep === 'restart') modalBodyText = t('restarting_robot') + if (updateStep === 'restart' && letUserExitUpdate) { + subProgressBarText = t('restart_taking_too_long', { robotName }) + } return ( - <> - {renderedImg} - - {!errorMessage ? t('robot_update_success') : errorMessage} - - + + ) : null + } + > + {hasStoppedUpdating ? ( + + + + ) : ( + + {modalBodyText} + + + {letUserExitUpdate && updateStep !== 'restart' ? ( + <> + {t('problem_during_update')}{' '} + setShowFileSelect(true) + } + > + {t('try_restarting_the_update')} + + {showFileSelect && ( + + )} + + ) : ( + subProgressBarText + )} + + + )} + ) } interface RobotUpdateProgressFooterProps { robotName: string + installRobotUpdate: (robotName: string) => void errorMessage?: string | null closeUpdateBuildroot?: () => void } function RobotUpdateProgressFooter({ robotName, + installRobotUpdate, errorMessage, closeUpdateBuildroot, }: RobotUpdateProgressFooterProps): JSX.Element { const { t } = useTranslation('device_settings') - const dispatch = useDispatch() const installUpdate = React.useCallback(() => { - dispatch(clearRobotUpdateSession()) - dispatch(startRobotUpdate(robotName)) + installRobotUpdate(robotName) }, [robotName]) - const { createLiveCommand } = useCreateLiveCommandMutation() - const idleCommand: SetStatusBarCreateCommand = { - commandType: 'setStatusBar', - params: { animation: 'idle' }, - } - - // Called if the update fails - const startIdleAnimationIfFailed = (): void => { - if (errorMessage) { - createLiveCommand({ - command: idleCommand, - waitUntilComplete: false, - }).catch((e: Error) => - console.warn(`cannot run status bar animation: ${e.message}`) - ) - } - } - - React.useEffect(startIdleAnimationIfFailed, []) - return ( {errorMessage && ( @@ -125,39 +222,87 @@ function RobotUpdateProgressFooter({ ) } -interface RobotUpdateProgressModalProps { - robotName: string - updateStep: UpdateStep - stepProgress: number | null - error?: string | null - closeUpdateBuildroot?: () => void +interface SuccessOrErrorProps { + errorMessage?: string | null } -export function RobotUpdateProgressModal({ - robotName, - updateStep, - stepProgress, - error, - closeUpdateBuildroot, -}: RobotUpdateProgressModalProps): JSX.Element { +function SuccessOrError({ errorMessage }: SuccessOrErrorProps): JSX.Element { const { t } = useTranslation('device_settings') - const dispatch = useDispatch() - const progressPercent = React.useRef(0) - const [previousUpdateStep, setPreviousUpdateStep] = React.useState< - string | null - >(null) - const completeRobotUpdateHandler = (): RobotUpdateAction => { - if (closeUpdateBuildroot != null) closeUpdateBuildroot() - return dispatch(clearRobotUpdateSession()) - } + const IMAGE_ALT = 'Welcome screen background image' + let renderedImg: JSX.Element + if (!errorMessage) + renderedImg = ( + {IMAGE_ALT} + ) + else + renderedImg = ( + + ) + return ( + <> + {renderedImg} + + {!errorMessage ? t('robot_update_success') : errorMessage} + + + ) +} + +export const TIME_BEFORE_ALLOWING_EXIT_MS = 600000 // 10 mins + +function useAllowExitIfUpdateStalled( + updateStep: UpdateStep, + progressPercent: number +): boolean { + const [letUserExitUpdate, setLetUserExitUpdate] = React.useState( + false + ) + const prevSeenUpdateProgress = React.useRef(null) + const exitTimeoutRef = React.useRef(null) + + React.useEffect(() => { + if (updateStep === 'initial' && prevSeenUpdateProgress.current !== null) { + prevSeenUpdateProgress.current = null + } else if (updateStep === 'finished' && exitTimeoutRef.current) { + clearTimeout(exitTimeoutRef.current) + setLetUserExitUpdate(false) + } else if (progressPercent !== prevSeenUpdateProgress.current) { + if (exitTimeoutRef.current) clearTimeout(exitTimeoutRef.current) + exitTimeoutRef.current = setTimeout(() => { + setLetUserExitUpdate(true) + }, TIME_BEFORE_ALLOWING_EXIT_MS) + + prevSeenUpdateProgress.current = progressPercent + setLetUserExitUpdate(false) + } + }, [progressPercent, updateStep]) + + React.useEffect(() => { + return () => { + if (exitTimeoutRef.current) clearTimeout(exitTimeoutRef.current) + } + }, []) + + return letUserExitUpdate +} + +function useStatusBarAnimation(isError: boolean): void { const { createLiveCommand } = useCreateLiveCommandMutation() const updatingCommand: SetStatusBarCreateCommand = { commandType: 'setStatusBar', params: { animation: 'updating' }, } + const idleCommand: SetStatusBarCreateCommand = { + commandType: 'setStatusBar', + params: { animation: 'idle' }, + } - // Called when the first step of the update begins const startUpdatingAnimation = (): void => { createLiveCommand({ command: updatingCommand, @@ -167,83 +312,26 @@ export function RobotUpdateProgressModal({ ) } - let modalBodyText = t('installing_update') - if (updateStep === 'restart') modalBodyText = t('restarting_robot') + const startIdleAnimationIfFailed = (): void => { + if (isError) { + createLiveCommand({ + command: idleCommand, + waitUntilComplete: false, + }).catch((e: Error) => + console.warn(`cannot run status bar animation: ${e.message}`) + ) + } + } - // Make sure to start the animation when this modal first pops up React.useEffect(startUpdatingAnimation, []) + React.useEffect(startIdleAnimationIfFailed, [isError]) +} - // Account for update methods that do not require download & decreasing percent oddities. +function useCleanupRobotUpdateSessionOnDismount(): void { + const dispatch = useDispatch() React.useEffect(() => { - const explicitStepProgress = stepProgress || 0 - if (previousUpdateStep === null) { - if (updateStep === 'install') - progressPercent.current = Math.max( - progressPercent.current, - explicitStepProgress - ) - else if (updateStep === 'download') { - progressPercent.current = Math.max( - progressPercent.current, - Math.floor(explicitStepProgress / 2) - ) - if (progressPercent.current === 50) setPreviousUpdateStep('download') - } else progressPercent.current = 100 - } else { - progressPercent.current = Math.max( - progressPercent.current, - 50 + Math.floor(explicitStepProgress / 2) - ) + return () => { + dispatch(clearRobotUpdateSession()) } - }, [updateStep, stepProgress, previousUpdateStep]) - - const completedUpdating = error || updateStep === 'finished' - - const UPDATE_PROGRESS_BAR_STYLE = css` - margin-top: ${SPACING.spacing24}; - margin-bottom: ${SPACING.spacing24}; - border-radius: ${BORDERS.borderRadiusSize3}; - background: ${COLORS.medGreyEnabled}; - ` - const dontTurnOffMessage = css` - color: ${COLORS.darkGreyEnabled}; - ` - - return ( - - ) : null - } - > - {completedUpdating ? ( - - - - ) : ( - - {modalBodyText} - - - {t('do_not_turn_off')} - - - )} - - ) + }, []) } diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx index 52a17eee2a9..674c30a480c 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx @@ -18,7 +18,6 @@ import { import { getRobotUpdateDisplayInfo, robotUpdateChangelogSeen, - startRobotUpdate, OT2_BALENA, UPGRADE, REINSTALL, @@ -29,6 +28,7 @@ import { useIsRobotBusy } from '../../hooks' import { Tooltip } from '../../../../atoms/Tooltip' import { LegacyModal } from '../../../../molecules/LegacyModal' import { Banner } from '../../../../atoms/Banner' +import { useDispatchStartRobotUpdate } from '../../../../redux/robot-update/hooks' import type { State, Dispatch } from '../../../../redux/types' import type { RobotSystemType } from '../../../../redux/robot-update/types' @@ -72,6 +72,7 @@ export function UpdateRobotModal({ const { updateFromFileDisabledReason } = useSelector((state: State) => { return getRobotUpdateDisplayInfo(state, robotName) }) + const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() const isRobotBusy = useIsRobotBusy() const updateDisabled = updateFromFileDisabledReason !== null || isRobotBusy @@ -106,7 +107,7 @@ export function UpdateRobotModal({ {updateType === UPGRADE ? t('remind_me_later') : t('not_now')} dispatch(startRobotUpdate(robotName))} + onClick={() => dispatchStartRobotUpdate(robotName)} marginRight={SPACING.spacing12} css={FOOTER_BUTTON_STYLE} disabled={updateDisabled} diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx index 1ed4e328e33..7b2207f0bb2 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx @@ -4,43 +4,40 @@ import { useSelector } from 'react-redux' import { OT2_BALENA, getRobotUpdateInfo, - getRobotUpdateDownloadProgress, getRobotUpdateDownloadError, + getRobotSystemType, + getRobotUpdateAvailable, } from '../../../../redux/robot-update' import { getAvailableShellUpdate } from '../../../../redux/shell' import { Portal } from '../../../../App/portal' import { UpdateAppModal } from '../../../../organisms/UpdateAppModal' import { MigrationWarningModal } from './MigrationWarningModal' -import { RobotUpdateProgressModal } from './RobotUpdateProgressModal' import { UpdateRobotModal } from './UpdateRobotModal' -import type { - RobotUpdateType, - RobotSystemType, -} from '../../../../redux/robot-update/types' import type { State } from '../../../../redux/types' +import { ReachableRobot, Robot } from '../../../../redux/discovery/types' export interface ViewUpdateModalProps { robotName: string - robotUpdateType: RobotUpdateType | null - robotSystemType: RobotSystemType | null + robot: Robot | ReachableRobot closeModal: () => void } export function ViewUpdateModal( props: ViewUpdateModalProps ): JSX.Element | null { - const { robotName, robotUpdateType, robotSystemType, closeModal } = props + const { robotName, robot, closeModal } = props const updateInfo = useSelector((state: State) => getRobotUpdateInfo(state, robotName) ) - const downloadProgress = useSelector((state: State) => - getRobotUpdateDownloadProgress(state, robotName) - ) const downloadError = useSelector((state: State) => getRobotUpdateDownloadError(state, robotName) ) + const robotUpdateType = useSelector((state: State) => + getRobotUpdateAvailable(state, robot) + ) + const robotSystemType = getRobotSystemType(robot) const availableAppUpdateVersion = useSelector(getAvailableShellUpdate) const [ @@ -73,16 +70,6 @@ export function ViewUpdateModal( ) } - if (updateInfo === null) - return ( - - ) - if (robotSystemType != null) return ( +const mockUseRobotUpdateInfo = useRobotUpdateInfo as jest.MockedFunction< + typeof useRobotUpdateInfo +> +const mockGetRobotSessionIsManualFile = getRobotSessionIsManualFile as jest.MockedFunction< + typeof getRobotSessionIsManualFile +> +const mockUseDispatchStartRobotUpdate = useDispatchStartRobotUpdate as jest.MockedFunction< + typeof useDispatchStartRobotUpdate +> const render = ( props: React.ComponentProps @@ -24,22 +42,36 @@ const render = ( } describe('DownloadUpdateModal', () => { + const mockRobotUpdateSession: RobotUpdateSession | null = { + robotName: 'testRobot', + fileInfo: null, + token: null, + pathPrefix: null, + step: 'getToken', + stage: 'validating', + progress: 50, + error: null, + } + let props: React.ComponentProps - let mockCreateLiveCommand = jest.fn() + const mockCreateLiveCommand = jest.fn() beforeEach(() => { - mockCreateLiveCommand = jest.fn() mockCreateLiveCommand.mockResolvedValue(null) props = { robotName: 'testRobot', - updateStep: 'download', - error: null, - stepProgress: 50, + session: mockRobotUpdateSession, closeUpdateBuildroot: jest.fn(), } mockUseCreateLiveCommandMutation.mockReturnValue({ createLiveCommand: mockCreateLiveCommand, } as any) + mockUseRobotUpdateInfo.mockReturnValue({ + updateStep: 'install', + progressPercent: 50, + }) + mockGetRobotSessionIsManualFile.mockReturnValue(false) + mockUseDispatchStartRobotUpdate.mockReturnValue(jest.fn) }) afterEach(() => { @@ -67,42 +99,36 @@ describe('DownloadUpdateModal', () => { }) it('renders the correct text when installing the robot update with no close button', () => { - props = { - ...props, - updateStep: 'install', - } - const [{ queryByRole, getByText }] = render(props) expect(getByText('Installing update...')).toBeInTheDocument() expect( - getByText('Do not turn off the robot while updating') + getByText("This could take up to 15 minutes. Don't turn off the robot.") ).toBeInTheDocument() expect(queryByRole('button')).not.toBeInTheDocument() }) it('renders the correct text when finalizing the robot update with no close button', () => { - props = { - ...props, + mockUseRobotUpdateInfo.mockReturnValue({ updateStep: 'restart', - } - + progressPercent: 100, + }) const [{ queryByRole, getByText }] = render(props) expect( getByText('Install complete, robot restarting...') ).toBeInTheDocument() expect( - getByText('Do not turn off the robot while updating') + getByText("This could take up to 15 minutes. Don't turn off the robot.") ).toBeInTheDocument() expect(queryByRole('button')).not.toBeInTheDocument() }) it('renders a success modal and exit button upon finishing the update process', () => { - props = { - ...props, + mockUseRobotUpdateInfo.mockReturnValue({ updateStep: 'finished', - } + progressPercent: 100, + }) const [{ getByText }] = render(props) const exitButton = getByText('exit') @@ -115,16 +141,19 @@ describe('DownloadUpdateModal', () => { }) it('renders an error modal and exit button if an error occurs', () => { + props = { + ...props, + session: { + ...mockRobotUpdateSession, + error: 'test error', + }, + } const idleCommand: SetStatusBarCreateCommand = { commandType: 'setStatusBar', params: { animation: 'idle' }, } - props = { - ...props, - error: 'test error', - } - const [{ getByText }] = render(props) + const [{ getByText }] = render(props) const exitButton = getByText('exit') expect(getByText('test error')).toBeInTheDocument() @@ -139,4 +168,15 @@ describe('DownloadUpdateModal', () => { waitUntilComplete: false, }) }) + + it('renders alternative text if update takes too long', () => { + const [{ findByText }] = render(props) + + act(() => { + jest.advanceTimersByTime(TIME_BEFORE_ALLOWING_EXIT_MS) + }) + + findByText('Try restarting the update.') + findByText('testRobot restart is taking longer than expected.') + }) }) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateBuildroot.test.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateBuildroot.test.tsx index 98ac277b92d..ed18668c9cf 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateBuildroot.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateBuildroot.test.tsx @@ -65,11 +65,9 @@ describe('UpdateBuildroot', () => { const viewUpdate = wrapper.find(ViewUpdateModal) expect(viewUpdate.prop('robotName')).toBe(mockRobot.name) - expect(viewUpdate.prop('robotUpdateType')).toBe(RobotUpdate.UPGRADE) + expect(viewUpdate.prop('robot')).toBe(mockRobot) - expect(getRobotUpdateAvailable).toHaveBeenCalledWith(MOCK_STATE, mockRobot) expect(closeModal).not.toHaveBeenCalled() - viewUpdate.invoke('closeModal')?.() expect(closeModal).toHaveBeenCalled() }) @@ -91,8 +89,5 @@ describe('UpdateBuildroot', () => { const progressModal = wrapper.find(RobotUpdateProgressModal) expect(progressModal.prop('robotName')).toBe(mockRobot.name) - expect(progressModal.prop('updateStep')).toBe('download') - expect(progressModal.prop('error')).toBe(mockSession.error) - expect(progressModal.prop('stepProgress')).toBe(mockSession.progress) }) }) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/ViewUpdateModal.test.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/ViewUpdateModal.test.tsx index 53717fb6685..5d732b6ddf7 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/ViewUpdateModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/ViewUpdateModal.test.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { QueryClient, QueryClientProvider } from 'react-query' import { mountWithStore } from '@opentrons/components' + import { useIsRobotBusy } from '../../../hooks' import * as RobotUpdate from '../../../../../redux/robot-update' - -import { RobotUpdateProgressModal } from '../RobotUpdateProgressModal' +import { mockConnectableRobot as mockRobot } from '../../../../../redux/discovery/__fixtures__' import { UpdateRobotModal } from '../UpdateRobotModal' import { MigrationWarningModal } from '../MigrationWarningModal' import { ViewUpdateModal } from '../ViewUpdateModal' @@ -27,6 +27,12 @@ const getRobotUpdateDownloadError = RobotUpdate.getRobotUpdateDownloadError as j const getRobotUpdateDisplayInfo = RobotUpdate.getRobotUpdateDisplayInfo as jest.MockedFunction< typeof RobotUpdate.getRobotUpdateDisplayInfo > +const getRobotSystemType = RobotUpdate.getRobotSystemType as jest.MockedFunction< + typeof RobotUpdate.getRobotSystemType +> +const getRobotUpdateAvailable = RobotUpdate.getRobotUpdateAvailable as jest.MockedFunction< + typeof RobotUpdate.getRobotUpdateAvailable +> const mockUseIsRobotBusy = useIsRobotBusy as jest.MockedFunction< typeof useIsRobotBusy @@ -39,20 +45,12 @@ const queryClient = new QueryClient() describe('ViewUpdateModal', () => { const handleClose = jest.fn() - const render = ( - robotUpdateType: React.ComponentProps< - typeof ViewUpdateModal - >['robotUpdateType'] = RobotUpdate.UPGRADE, - robotSystemType: React.ComponentProps< - typeof ViewUpdateModal - >['robotSystemType'] = RobotUpdate.OT2_BUILDROOT - ) => { + const render = () => { return mountWithStore>( , @@ -68,6 +66,8 @@ describe('ViewUpdateModal', () => { autoUpdateDisabledReason: null, updateFromFileDisabledReason: null, }) + getRobotSystemType.mockReturnValue(RobotUpdate.FLEX) + getRobotUpdateAvailable.mockReturnValue(RobotUpdate.UPGRADE) mockUseIsRobotBusy.mockReturnValue(false) }) @@ -75,31 +75,6 @@ describe('ViewUpdateModal', () => { jest.resetAllMocks() }) - it('renders a RobotUpdateProgressModal if the update has not completed downloading', () => { - const { wrapper } = render() - const robotUpdateProgressModal = wrapper.find(RobotUpdateProgressModal) - - expect(robotUpdateProgressModal.prop('error')).toEqual(null) - expect(robotUpdateProgressModal.prop('stepProgress')).toEqual(50) - expect(getRobotUpdateDownloadProgress).toHaveBeenCalledWith( - MOCK_STATE, - 'robot-name' - ) - }) - - it('should show a RobotUpdateProgressModal if the update download errored out', () => { - getRobotUpdateDownloadError.mockReturnValue('oh no!') - - const { wrapper } = render() - const robotUpdateProgressModal = wrapper.find(RobotUpdateProgressModal) - - expect(robotUpdateProgressModal.prop('error')).toEqual('oh no!') - expect(getRobotUpdateDownloadError).toHaveBeenCalledWith( - MOCK_STATE, - 'robot-name' - ) - }) - it('should show a UpdateRobotModal if the update is an upgrade', () => { getRobotUpdateInfo.mockReturnValue({ version: '1.0.0', @@ -112,12 +87,13 @@ describe('ViewUpdateModal', () => { expect(updateRobotModal.prop('robotName')).toBe(MOCK_ROBOT_NAME) expect(updateRobotModal.prop('releaseNotes')).toBe('hey look a release') - expect(updateRobotModal.prop('systemType')).toBe(RobotUpdate.OT2_BUILDROOT) + expect(updateRobotModal.prop('systemType')).toBe(RobotUpdate.FLEX) expect(getRobotUpdateInfo).toHaveBeenCalledWith(MOCK_STATE, 'robot-name') }) it('should show a MigrationWarningModal if the robot is on Balena', () => { - const { wrapper } = render(RobotUpdate.UPGRADE, RobotUpdate.OT2_BALENA) + getRobotSystemType.mockReturnValue(RobotUpdate.OT2_BALENA) + const { wrapper } = render() const migrationWarning = wrapper.find(MigrationWarningModal) expect(migrationWarning.prop('updateType')).toBe(RobotUpdate.UPGRADE) @@ -136,8 +112,8 @@ describe('ViewUpdateModal', () => { target: 'ot2', releaseNotes: 'hey look a release', }) - - const { wrapper } = render(RobotUpdate.UPGRADE, RobotUpdate.OT2_BALENA) + getRobotSystemType.mockReturnValue(RobotUpdate.OT2_BALENA) + const { wrapper } = render() const migrationWarning = wrapper.find(MigrationWarningModal) migrationWarning.invoke('proceed')?.() @@ -147,11 +123,12 @@ describe('ViewUpdateModal', () => { ) }) - it('should proceed from MigrationWarningModal to RobotUpdateProgressModal if still downloading', () => { - const { wrapper } = render(RobotUpdate.UPGRADE, RobotUpdate.OT2_BALENA) + it('should proceed from MigrationWarningModal to UpdateRobotModal', () => { + getRobotSystemType.mockReturnValue(RobotUpdate.OT2_BALENA) + const { wrapper } = render() const migrationWarning = wrapper.find(MigrationWarningModal) migrationWarning.invoke('proceed')?.() - expect(wrapper.exists(RobotUpdateProgressModal)).toBe(true) + expect(wrapper.exists(UpdateRobotModal)).toBe(true) }) }) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx new file mode 100644 index 00000000000..f884700a619 --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx @@ -0,0 +1,95 @@ +import { renderHook } from '@testing-library/react-hooks' +import { useRobotUpdateInfo } from '../useRobotUpdateInfo' +import { RobotUpdateSession } from '../../../../../redux/robot-update/types' + +describe('useRobotUpdateInfo', () => { + const mockRobotUpdateSession: RobotUpdateSession | null = { + robotName: 'testRobot', + fileInfo: { isManualFile: true, systemFile: 'testFile', version: '1.0.0' }, + token: null, + pathPrefix: null, + step: 'getToken', + stage: 'validating', + progress: 50, + error: null, + } + + it('should return initial values when session is null', () => { + const { result } = renderHook(() => useRobotUpdateInfo(null)) + + expect(result.current.updateStep).toBe('initial') + expect(result.current.progressPercent).toBe(0) + }) + + it('should return initial values when there is no session step and stage', () => { + const { result } = renderHook(session => useRobotUpdateInfo(session), { + initialProps: { + ...mockRobotUpdateSession, + step: null, + stage: null, + }, + }) + + expect(result.current.updateStep).toBe('initial') + expect(result.current.progressPercent).toBe(0) + }) + + it('should update updateStep and progressPercent when session is provided', () => { + const { result, rerender } = renderHook( + session => useRobotUpdateInfo(session), + { + initialProps: mockRobotUpdateSession, + } + ) + + expect(result.current.updateStep).toBe('install') + expect(result.current.progressPercent).toBe(25) + + rerender({ + ...mockRobotUpdateSession, + step: 'restart', + stage: 'ready-for-restart', + progress: 100, + error: null, + }) + + expect(result.current.updateStep).toBe('restart') + expect(result.current.progressPercent).toBe(100) + }) + + it('should return correct updateStep and progressPercent values when there is an error', () => { + const { result, rerender } = renderHook( + session => useRobotUpdateInfo(session), + { + initialProps: mockRobotUpdateSession, + } + ) + + expect(result.current.updateStep).toBe('install') + expect(result.current.progressPercent).toBe(25) + + rerender({ + ...mockRobotUpdateSession, + error: 'Something went wrong', + }) + + expect(result.current.updateStep).toBe('error') + expect(result.current.progressPercent).toBe(25) + }) + + it('should calculate correct progressPercent when the update is not manual', () => { + const { result } = renderHook(session => useRobotUpdateInfo(session), { + initialProps: { + ...mockRobotUpdateSession, + fileInfo: { + systemFile: 'downloadPath', + version: '1.0.0', + isManualFile: false, + }, + }, + }) + + expect(result.current.updateStep).toBe('install') + expect(result.current.progressPercent).toBe(25) + }) +}) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/index.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/index.tsx index f57031d6e92..4498aa514ee 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/index.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/index.tsx @@ -4,69 +4,57 @@ import { setRobotUpdateSeen, robotUpdateIgnored, getRobotUpdateSession, - getRobotSystemType, - getRobotUpdateAvailable, } from '../../../../redux/robot-update' import { ViewUpdateModal } from './ViewUpdateModal' import { RobotUpdateProgressModal } from './RobotUpdateProgressModal' +import { UNREACHABLE } from '../../../../redux/discovery' -import type { State, Dispatch } from '../../../../redux/types' -import type { ViewableRobot } from '../../../../redux/discovery/types' - -export type UpdateStep = 'download' | 'install' | 'restart' | 'finished' +import type { Dispatch } from '../../../../redux/types' +import type { DiscoveredRobot } from '../../../../redux/discovery/types' export interface UpdateBuildrootProps { - robot: ViewableRobot close: () => void + robot?: DiscoveredRobot | null } -export function UpdateBuildroot(props: UpdateBuildrootProps): JSX.Element { +export function UpdateBuildroot( + props: UpdateBuildrootProps +): JSX.Element | null { const { robot, close } = props - const robotName = robot.name - const session = useSelector(getRobotUpdateSession) - const robotUpdateType = useSelector((state: State) => - getRobotUpdateAvailable(state, robot) - ) + const hasSeenSessionOnce = React.useRef(false) + const robotName = React.useRef(robot?.name ?? '') const dispatch = useDispatch() - const { step, error: installError } = session || { - step: null, - installError: null, - } + const session = useSelector(getRobotUpdateSession) + if (!hasSeenSessionOnce.current && session) hasSeenSessionOnce.current = true - // set update seen on component mount React.useEffect(() => { - dispatch(setRobotUpdateSeen(robotName)) + if (robotName.current) { + dispatch(setRobotUpdateSeen(robotName.current)) + } }, [robotName]) const ignoreUpdate = React.useCallback(() => { - dispatch(robotUpdateIgnored(robotName)) + if (robotName.current) { + dispatch(robotUpdateIgnored(robotName.current)) + } close() }, [robotName, close]) - const robotSystemType = getRobotSystemType(robot) - - // TODO(jh, 09-14-2023: add download logic to app-shell/redux/progress bar. - let updateStep: UpdateStep - const DOWNLOAD_STEPS = ['getToken'] - if (step == null || DOWNLOAD_STEPS.includes(step)) updateStep = 'download' - else if (step === 'finished') updateStep = 'finished' - else if (step === 'restart' || step === 'restarting') updateStep = 'restart' - else updateStep = 'install' - - return session ? ( - - ) : ( - - ) + if (hasSeenSessionOnce.current) + return ( + + ) + else if (robot != null && robot.status !== UNREACHABLE) + return ( + + ) + else return null } diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/useRobotUpdateInfo.ts b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/useRobotUpdateInfo.ts new file mode 100644 index 00000000000..c77e6ac6a8a --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/useRobotUpdateInfo.ts @@ -0,0 +1,147 @@ +import * as React from 'react' +import type { RobotUpdateSession } from '../../../../redux/robot-update/types' + +export function useRobotUpdateInfo( + session: RobotUpdateSession | null +): { updateStep: UpdateStep; progressPercent: number } { + const shellReportedUpdateStep = React.useMemo( + () => getShellReportedUpdateStep(session), + [session] + ) + + const updateStep = useTransitionUpdateStepFrom(shellReportedUpdateStep) + const progressPercent = useFindProgressPercentFrom( + updateStep, + session?.fileInfo?.isManualFile ?? false, + session?.progress + ) + + return { + updateStep, + progressPercent, + } +} + +export type UpdateStep = + | 'initial' + | 'download' + | 'install' + | 'restart' + | 'finished' + | 'error' + +function getShellReportedUpdateStep( + session: RobotUpdateSession | null +): UpdateStep | null { + if (session == null) return null + const { step: sessionStep, stage: sessionStage, error } = session + + // TODO(jh, 09-14-2023: add download logic to app-shell/redux/progress bar. + let reportedUpdateStep: UpdateStep + if (error != null) { + reportedUpdateStep = 'error' + } else if (sessionStep == null && sessionStage == null) { + reportedUpdateStep = 'initial' + } else if (sessionStep === 'finished') { + reportedUpdateStep = 'finished' + } else if ( + sessionStep === 'restart' || + sessionStep === 'restarting' || + sessionStage === 'ready-for-restart' + ) { + reportedUpdateStep = 'restart' + } else { + reportedUpdateStep = 'install' + } + + return reportedUpdateStep +} + +function useTransitionUpdateStepFrom( + reportedUpdateStep: UpdateStep | null +): UpdateStep { + const [updateStep, setUpdateStep] = React.useState('initial') + const prevUpdateStep = React.useRef(null) + + if (reportedUpdateStep === 'initial' && updateStep !== 'initial') { + setUpdateStep('initial') + prevUpdateStep.current = null + } else if (reportedUpdateStep === 'error' && updateStep !== 'error') { + setUpdateStep('error') + } else if (updateStep === 'initial') { + if (reportedUpdateStep === 'install' && prevUpdateStep.current == null) { + setUpdateStep('install') + prevUpdateStep.current = 'initial' + } + } else if (updateStep === 'install') { + if ( + reportedUpdateStep === 'restart' && + prevUpdateStep.current === 'initial' + ) { + setUpdateStep('restart') + prevUpdateStep.current = 'install' + } + } else if (updateStep === 'restart') { + if ( + reportedUpdateStep === 'finished' && + prevUpdateStep.current === 'install' + ) { + setUpdateStep('finished') + prevUpdateStep.current = 'restart' + } + } + return updateStep +} + +function useFindProgressPercentFrom( + updateStep: UpdateStep, + filePathUsedForUpdate: boolean, + stepProgress?: number | null +): number { + const [progressPercent, setProgressPercent] = React.useState(0) + const prevSeenStepProgress = React.useRef(0) + const currentStepWithProgress = React.useRef(0) + + const TOTAL_STEPS_WITH_PROGRESS = filePathUsedForUpdate ? 2 : 2 + + if (updateStep === 'initial') { + if (progressPercent !== 0) { + setProgressPercent(0) + prevSeenStepProgress.current = 0 + } + } else if (updateStep === 'restart' || updateStep === 'finished') { + if (progressPercent !== 100) { + setProgressPercent(100) + prevSeenStepProgress.current = 100 + } + return progressPercent + } else if ( + updateStep === 'error' || + stepProgress == null || + stepProgress === prevSeenStepProgress.current + ) { + return progressPercent + } else { + // Assume new step instead of stepProgress backtracking. + if (prevSeenStepProgress.current > 85 && stepProgress <= 60) { + currentStepWithProgress.current += 1 + const completedStepsWithProgress = + (100 * currentStepWithProgress.current) / TOTAL_STEPS_WITH_PROGRESS + prevSeenStepProgress.current = 0 + setProgressPercent(Math.round(completedStepsWithProgress + stepProgress)) + } else { + const currentStepProgress = + progressPercent + + (stepProgress - prevSeenStepProgress.current) / + TOTAL_STEPS_WITH_PROGRESS + const nonBacktrackedProgressPercent = Math.max( + progressPercent, + currentStepProgress + ) + setProgressPercent(Math.round(nonBacktrackedProgressPercent)) + } + prevSeenStepProgress.current = stepProgress + } + + return progressPercent +} diff --git a/app/src/organisms/UpdateAppModal/index.tsx b/app/src/organisms/UpdateAppModal/index.tsx index 43a46c8e63b..5058a207360 100644 --- a/app/src/organisms/UpdateAppModal/index.tsx +++ b/app/src/organisms/UpdateAppModal/index.tsx @@ -34,7 +34,6 @@ interface PlaceHolderErrorProps { errorMessage?: string } -// TODO(jh, 2023-08-25): refactor default error handling into LegacyModal const PlaceholderError = ({ errorMessage, }: PlaceHolderErrorProps): JSX.Element => { @@ -74,9 +73,14 @@ const UpdateAppBanner = styled(Banner)` border: none; ` const UPDATE_PROGRESS_BAR_STYLE = css` - margin-top: 1.5rem; + margin-top: ${SPACING.spacing24}; border-radius: ${BORDERS.borderRadiusSize3}; background: ${COLORS.medGreyEnabled}; + width: 17.12rem; +` +const LEGACY_MODAL_STYLE = css` + width: 40rem; + textalign: center; ` export interface UpdateAppModalProps { @@ -127,12 +131,16 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { return ( <> {error != null ? ( - closeModal(true)}> + closeModal(true)} + css={LEGACY_MODAL_STYLE} + > ) : null} {(downloading || downloaded) && error == null ? ( - + diff --git a/app/src/organisms/UpdateRobotBanner/index.tsx b/app/src/organisms/UpdateRobotBanner/index.tsx index 526f5469f1f..166b92e7190 100644 --- a/app/src/organisms/UpdateRobotBanner/index.tsx +++ b/app/src/organisms/UpdateRobotBanner/index.tsx @@ -8,10 +8,8 @@ import { Btn, DIRECTION_COLUMN, } from '@opentrons/components' -import { Portal } from '../../App/portal' import { StyledText } from '../../atoms/text' import { Banner } from '../../atoms/Banner' -import { UNREACHABLE } from '../../redux/discovery' import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' import { UpdateBuildroot } from '../Devices/RobotSettings/UpdateBuildroot' @@ -59,15 +57,11 @@ export function UpdateRobotBanner( {t('view_update')} - {showSoftwareUpdateModal && - robot != null && - robot.status !== UNREACHABLE ? ( - - setShowSoftwareUpdateModal(false)} - /> - + {showSoftwareUpdateModal ? ( + setShowSoftwareUpdateModal(false)} + /> ) : null} ) : null diff --git a/app/src/redux/robot-admin/__tests__/selectors.test.ts b/app/src/redux/robot-admin/__tests__/selectors.test.ts index 392a618c94c..a3f9764f6ce 100644 --- a/app/src/redux/robot-admin/__tests__/selectors.test.ts +++ b/app/src/redux/robot-admin/__tests__/selectors.test.ts @@ -5,8 +5,9 @@ import { getNextRestartStatus, getResetConfigOptions, } from '../selectors' -import type { State } from '../../types' import { ConnectivityStatus } from '../../discovery/types' +import { RESTART_TIMEOUT_SEC } from '../constants' +import type { State } from '../../types' const START_TIME = new Date('2000-01-01') @@ -199,8 +200,8 @@ describe('robot admin selectors', () => { it('should return restart timed out if it takes too long', () => { const startTime = new Date('2000-01-01') - const notLongEnough = add(startTime, { seconds: 299 }) - const tooLong = add(startTime, { seconds: 301 }) + const notLongEnough = add(startTime, { seconds: RESTART_TIMEOUT_SEC - 1 }) + const tooLong = add(startTime, { seconds: RESTART_TIMEOUT_SEC + 1 }) const state: State = { robotAdmin: { diff --git a/app/src/redux/robot-admin/constants.ts b/app/src/redux/robot-admin/constants.ts index 64ce3d6869e..f918508483f 100644 --- a/app/src/redux/robot-admin/constants.ts +++ b/app/src/redux/robot-admin/constants.ts @@ -1,7 +1,7 @@ // general constants -// 5 minute restart timeout -export const RESTART_TIMEOUT_SEC = 300 +// 15 minute restart timeout +export const RESTART_TIMEOUT_SEC = 900 // restart statuses export const RESTART_PENDING_STATUS: 'restart-pending' = 'restart-pending' diff --git a/app/src/redux/robot-update/__tests__/hooks.test.ts b/app/src/redux/robot-update/__tests__/hooks.test.ts new file mode 100644 index 00000000000..b1399e33f5d --- /dev/null +++ b/app/src/redux/robot-update/__tests__/hooks.test.ts @@ -0,0 +1,35 @@ +import { useDispatch } from 'react-redux' +import { renderHook } from '@testing-library/react-hooks' + +import { useDispatchStartRobotUpdate } from '../hooks' +import { startRobotUpdate, clearRobotUpdateSession } from '../actions' + +jest.mock('react-redux') + +const mockUseDispatch = useDispatch as jest.MockedFunction + +describe('useDispatchStartRobotUpdate', () => { + let mockDispatch: jest.Mock + const mockRobotName = 'robotName' + const mockSystemFile = 'systemFile' + + beforeEach(() => { + mockDispatch = jest.fn() + mockUseDispatch.mockReturnValue(mockDispatch) + }) + + afterEach(() => { + mockUseDispatch.mockClear() + jest.clearAllMocks() + }) + + it('clears the robot update session before dispatching a new session with the given robotName and systemFile', () => { + const { result } = renderHook(useDispatchStartRobotUpdate) + + result.current(mockRobotName, mockSystemFile) + expect(mockDispatch).toHaveBeenCalledWith(clearRobotUpdateSession()) + expect(mockDispatch).toHaveBeenCalledWith( + startRobotUpdate(mockRobotName, mockSystemFile) + ) + }) +}) diff --git a/app/src/redux/robot-update/__tests__/selectors.test.ts b/app/src/redux/robot-update/__tests__/selectors.test.ts index 81df4498f2f..ea834b94c02 100644 --- a/app/src/redux/robot-update/__tests__/selectors.test.ts +++ b/app/src/redux/robot-update/__tests__/selectors.test.ts @@ -359,6 +359,17 @@ describe('robot update selectors', () => { }) }) + it('should get whether the robot is updated via a manual file', () => { + const state: State = { + robotUpdate: { + session: { fileInfo: { isManualFile: true } }, + }, + } as any + const result = selectors.getRobotSessionIsManualFile(state) + + expect(result).toBeTruthy() + }) + it('should get the robot from session after migration with opentrons- name prefix', () => { const state: State = { robotUpdate: { diff --git a/app/src/redux/robot-update/hooks.ts b/app/src/redux/robot-update/hooks.ts new file mode 100644 index 00000000000..840349ef331 --- /dev/null +++ b/app/src/redux/robot-update/hooks.ts @@ -0,0 +1,22 @@ +import { useDispatch } from 'react-redux' +import { startRobotUpdate, clearRobotUpdateSession } from './actions' +import type { Action } from '../types' + +type DispatchStartRobotUpdate = ( + robotName: string, + systemFile?: string | undefined +) => void + +export function useDispatchStartRobotUpdate(): DispatchStartRobotUpdate { + const dispatch = useDispatch<(a: Action) => void>() + + function dispatchStartRobotUpdate( + robotName: string, + systemFile?: string + ): void { + dispatch(clearRobotUpdateSession()) + dispatch(startRobotUpdate(robotName, systemFile)) + } + + return dispatchStartRobotUpdate +} diff --git a/app/src/redux/robot-update/selectors.ts b/app/src/redux/robot-update/selectors.ts index fcba6c08127..e676511fe95 100644 --- a/app/src/redux/robot-update/selectors.ts +++ b/app/src/redux/robot-update/selectors.ts @@ -115,6 +115,10 @@ export function getRobotUpdateSessionRobotName(state: State): string | null { return state.robotUpdate.session?.robotName || null } +export function getRobotSessionIsManualFile(state: State): boolean | null { + return state.robotUpdate.session?.fileInfo?.isManualFile ?? null +} + export const getRobotUpdateRobot: ( state: State ) => ViewableRobot | null = createSelector( diff --git a/app/src/redux/robot-update/types.ts b/app/src/redux/robot-update/types.ts index 0f75085bd87..d2dde216a91 100644 --- a/app/src/redux/robot-update/types.ts +++ b/app/src/redux/robot-update/types.ts @@ -16,6 +16,7 @@ export interface RobotUpdateInfo { export interface RobotUpdateFileInfo { systemFile: string version: string + isManualFile: boolean } // stage response from API From 041f12dafc62a57d3ffb5ebb98032e134dfe4c8c Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Fri, 6 Oct 2023 11:21:28 -0400 Subject: [PATCH 34/79] fix(app): show pipette settings in pipette settings slideout (#13726) Closes RQA-1223 --- .../PipetteCard/PipetteSettingsSlideout.tsx | 24 ++++--------------- .../__tests__/PipetteCard.test.tsx | 21 +++++++--------- .../organisms/Devices/PipetteCard/index.tsx | 20 ++++++++-------- 3 files changed, 23 insertions(+), 42 deletions(-) diff --git a/app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx b/app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx index ba1933dc85d..80438a922f5 100644 --- a/app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx +++ b/app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx @@ -1,13 +1,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import last from 'lodash/last' -import { useDispatch, useSelector } from 'react-redux' -import { Flex, useInterval } from '@opentrons/components' +import { useSelector } from 'react-redux' +import { Flex } from '@opentrons/components' import { PipetteModelSpecs } from '@opentrons/shared-data' -import { - fetchPipetteSettings, - updatePipetteSettings, -} from '../../../redux/pipettes' +import { updatePipetteSettings } from '../../../redux/pipettes' import { Slideout } from '../../../atoms/Slideout' import { getRequestById, @@ -22,10 +19,7 @@ import type { PipetteSettingsFieldsUpdate, PipetteSettingsFieldsMap, } from '../../../redux/pipettes/types' -import type { Dispatch, State } from '../../../redux/types' - -const FETCH_PIPETTES_INTERVAL_MS = 5000 - +import type { State } from '../../../redux/types' interface PipetteSettingsSlideoutProps { robotName: string pipetteName: PipetteModelSpecs['displayName'] @@ -47,7 +41,6 @@ export const PipetteSettingsSlideout = ( settings, } = props const { t } = useTranslation('device_details') - const dispatch = useDispatch() const [dispatchRequest, requestIds] = useDispatchApiRequest() const updateSettings = (fields: PipetteSettingsFieldsUpdate): void => { dispatchRequest(updatePipetteSettings(robotName, pipetteId, fields)) @@ -58,15 +51,6 @@ export const PipetteSettingsSlideout = ( ) const FORM_ID = `configurePipetteForm_${pipetteId}` - // TODO(bc, 2023-02-10): replace this with the usePipetteSettingsQuery for poll and data access in the child components - useInterval( - () => { - dispatch(fetchPipetteSettings(robotName)) - }, - FETCH_PIPETTES_INTERVAL_MS, - true - ) - return ( const mockUseCurrentSubsystemUpdateQuery = useCurrentSubsystemUpdateQuery as jest.MockedFunction< typeof useCurrentSubsystemUpdateQuery > -const mockGetAttachedPipetteSettingsFieldsById = getAttachedPipetteSettingsFieldsById as jest.MockedFunction< - typeof getAttachedPipetteSettingsFieldsById +const mockUsePipetteSettingsQuery = usePipetteSettingsQuery as jest.MockedFunction< + typeof usePipetteSettingsQuery > const render = (props: React.ComponentProps) => { @@ -113,9 +113,9 @@ describe('PipetteCard', () => { mockUseCurrentSubsystemUpdateQuery.mockReturnValue({ data: undefined, } as any) - when(mockGetAttachedPipetteSettingsFieldsById) - .calledWith({} as State, mockRobotName, 'id') - .mockReturnValue(mockPipetteSettingsFieldsMap) + when(mockUsePipetteSettingsQuery) + .calledWith({ refetchInterval: 5000, enabled: true }) + .mockReturnValue({} as any) }) afterEach(() => { jest.resetAllMocks() @@ -296,9 +296,6 @@ describe('PipetteCard', () => { getByText('Firmware update in progress...') }) it('does not render a pipette settings slideout card if the pipette has no settings', () => { - when(mockGetAttachedPipetteSettingsFieldsById) - .calledWith({} as State, mockRobotName, 'id') - .mockReturnValue(null) const { queryByTestId } = render(props) expect( queryByTestId( diff --git a/app/src/organisms/Devices/PipetteCard/index.tsx b/app/src/organisms/Devices/PipetteCard/index.tsx index ece39b2c202..f7b913a51d3 100644 --- a/app/src/organisms/Devices/PipetteCard/index.tsx +++ b/app/src/organisms/Devices/PipetteCard/index.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' -import { useSelector } from 'react-redux' import { Box, @@ -22,12 +21,12 @@ import { NINETY_SIX_CHANNEL, SINGLE_MOUNT_PIPETTES, } from '@opentrons/shared-data' -import { useCurrentSubsystemUpdateQuery } from '@opentrons/react-api-client' - import { - LEFT, - getAttachedPipetteSettingsFieldsById, -} from '../../../redux/pipettes' + useCurrentSubsystemUpdateQuery, + usePipetteSettingsQuery, +} from '@opentrons/react-api-client' + +import { LEFT } from '../../../redux/pipettes' import { OverflowBtn } from '../../../atoms/MenuList/OverflowBtn' import { StyledText } from '../../../atoms/text' import { Banner } from '../../../atoms/Banner' @@ -42,7 +41,6 @@ import { PipetteOverflowMenu } from './PipetteOverflowMenu' import { PipetteSettingsSlideout } from './PipetteSettingsSlideout' import { AboutPipetteSlideout } from './AboutPipetteSlideout' -import type { State } from '../../../redux/types' import type { PipetteModelSpecs, PipetteMount, @@ -125,9 +123,11 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { refetchInterval: SUBSYSTEM_UPDATE_POLL_MS, } ) - const settings = useSelector((state: State) => - getAttachedPipetteSettingsFieldsById(state, robotName, pipetteId ?? '') - ) + const settings = + usePipetteSettingsQuery({ + refetchInterval: 5000, + enabled: pipetteId != null, + })?.data?.[pipetteId ?? '']?.fields ?? null const [ selectedPipette, From 1adcabd749967060bab9b5b8f9c914d173953ebe Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 6 Oct 2023 11:59:28 -0400 Subject: [PATCH 35/79] fix(app): truncate protocol receipt toast to 30 characters (#13734) Truncate protocol receipt toast on app to "Successfully received {protocol name truncated to 30characters}..." for long protocol names --- app/src/App/hooks.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index 429f5a14283..bc574975776 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -5,7 +5,7 @@ import { useQueryClient } from 'react-query' import { useRouteMatch } from 'react-router-dom' import { useDispatch } from 'react-redux' -import { useInterval } from '@opentrons/components' +import { useInterval, truncateString } from '@opentrons/components' import { useAllProtocolIdsQuery, useAllRunsQuery, @@ -91,12 +91,13 @@ export function useProtocolReceiptToast(): void { protocolNames.forEach(name => { makeToast( t('protocol_added', { - protocol_name: name, + protocol_name: truncateString(name, 30), }), 'success', { closeButton: true, disableTimeout: true, + displayType: 'odd' } ) }) From 410a5fde37373d08325250e2ec88827d95c61f13 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Fri, 6 Oct 2023 16:51:27 -0400 Subject: [PATCH 36/79] fix(app): only show leveling screen for gen2 multi-channel pipettes (#13729) fix RQA-1735 --- app/src/organisms/ChangePipette/ConfirmPipette.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/organisms/ChangePipette/ConfirmPipette.tsx b/app/src/organisms/ChangePipette/ConfirmPipette.tsx index eb465d058b4..0e6e46211a1 100644 --- a/app/src/organisms/ChangePipette/ConfirmPipette.tsx +++ b/app/src/organisms/ChangePipette/ConfirmPipette.tsx @@ -111,7 +111,8 @@ export function ConfirmPipette(props: ConfirmPipetteProps): JSX.Element { return !confirmPipetteLevel && (wrongWantedPipette != null || (props.wantedPipette != null && success)) && actualPipette != null && - actualPipette.channels === 8 ? ( + actualPipette.channels === 8 && + actualPipette.displayCategory === 'GEN2' ? ( Date: Fri, 6 Oct 2023 17:00:47 -0400 Subject: [PATCH 37/79] fix(app): clear redux wifi status data on disconnect (#13733) provides a fallback to clear redux wifi interface data when the wifi disconnect modal is in a state where it has presumed a successful disconnection closes RQA-1745 --- .../ConnectNetwork/DisconnectModal.tsx | 7 ++++ .../__tests__/DisconnectModal.test.tsx | 9 +++++ .../networking/__tests__/actions.test.ts | 9 +++++ .../networking/__tests__/reducer.test.ts | 23 ++++++++++++ app/src/redux/networking/actions.ts | 7 ++++ app/src/redux/networking/constants.ts | 5 +++ app/src/redux/networking/reducer.ts | 37 +++++++++++++++++++ app/src/redux/networking/types.ts | 6 +++ 8 files changed, 103 insertions(+) diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx index cc68dca6477..662dbdce9b5 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx @@ -22,6 +22,7 @@ import { LegacyModal } from '../../../../molecules/LegacyModal' import { useRobot } from '../../../../organisms/Devices/hooks' import { CONNECTABLE } from '../../../../redux/discovery' import { + clearWifiStatus, getNetworkInterfaces, postWifiDisconnect, } from '../../../../redux/networking' @@ -112,6 +113,12 @@ export const DisconnectModal = ({ disconnectModalBody = t('disconnect_from_wifi_network_failure', { ssid }) } + React.useEffect(() => { + if (isDisconnected) { + dispatch(clearWifiStatus(robotName)) + } + }, [isDisconnected]) + return ( const mockUseRobot = useRobot as jest.MockedFunction +const mockClearWifiStatus = clearWifiStatus as jest.MockedFunction< + typeof clearWifiStatus +> const ROBOT_NAME = 'otie' const LAST_ID = 'a request id' @@ -117,6 +121,7 @@ describe('DisconnectModal', () => { getByText('Disconnect from foo') getByText('Disconnecting from Wi-Fi network foo') getByRole('button', { name: 'cancel' }) + expect(mockClearWifiStatus).not.toHaveBeenCalled() }) it('renders success body when request is pending and robot is not connectable', () => { @@ -133,6 +138,7 @@ describe('DisconnectModal', () => { 'Your robot has successfully disconnected from the Wi-Fi network.' ) getByRole('button', { name: 'Done' }) + expect(mockClearWifiStatus).toHaveBeenCalled() }) it('renders success body when request is successful', () => { @@ -146,6 +152,7 @@ describe('DisconnectModal', () => { 'Your robot has successfully disconnected from the Wi-Fi network.' ) getByRole('button', { name: 'Done' }) + expect(mockClearWifiStatus).toHaveBeenCalled() }) it('renders success body when wifi is not connected', () => { @@ -162,6 +169,7 @@ describe('DisconnectModal', () => { 'Your robot has successfully disconnected from the Wi-Fi network.' ) getByRole('button', { name: 'Done' }) + expect(mockClearWifiStatus).toHaveBeenCalled() }) it('renders error body when request is unsuccessful', () => { @@ -181,6 +189,7 @@ describe('DisconnectModal', () => { ) getByRole('button', { name: 'cancel' }) getByRole('button', { name: 'Disconnect' }) + expect(mockClearWifiStatus).not.toHaveBeenCalled() }) it('dispatches postWifiDisconnect on click Disconnect', () => { diff --git a/app/src/redux/networking/__tests__/actions.test.ts b/app/src/redux/networking/__tests__/actions.test.ts index 5e7c7207608..2c343fb8162 100644 --- a/app/src/redux/networking/__tests__/actions.test.ts +++ b/app/src/redux/networking/__tests__/actions.test.ts @@ -265,6 +265,15 @@ describe('networking actions', () => { meta: mockRequestMeta, }, }, + { + name: 'can create networking:CLEAR_WIFI_STATUS', + creator: Actions.clearWifiStatus, + args: [mockRobot.name], + expected: { + type: 'networking:CLEAR_WIFI_STATUS', + payload: { robotName: mockRobot.name }, + }, + }, ] SPECS.forEach(spec => { diff --git a/app/src/redux/networking/__tests__/reducer.test.ts b/app/src/redux/networking/__tests__/reducer.test.ts index e8512feea54..187a30b31a7 100644 --- a/app/src/redux/networking/__tests__/reducer.test.ts +++ b/app/src/redux/networking/__tests__/reducer.test.ts @@ -206,6 +206,29 @@ const SPECS: ReducerSpec[] = [ }, }, }, + + { + name: 'handles clear wifi status action', + action: Actions.clearWifiStatus(ROBOT_NAME), + state: { + [ROBOT_NAME]: Fixtures.mockNetworkingStatus, + }, + expected: { + [ROBOT_NAME]: { + ...Fixtures.mockNetworkingStatus, + interfaces: { + ...Fixtures.mockNetworkingStatus.interfaces, + wlan0: { + ipAddress: null, + macAddress: 'unknown', + gatewayAddress: null, + state: 'disconnected', + type: 'wifi', + }, + }, + }, + }, + }, ] describe('networkingReducer', () => { diff --git a/app/src/redux/networking/actions.ts b/app/src/redux/networking/actions.ts index c89be1724ef..49f6911605b 100644 --- a/app/src/redux/networking/actions.ts +++ b/app/src/redux/networking/actions.ts @@ -174,3 +174,10 @@ export const postWifiDisconnectFailure = ( payload: { robotName, error }, meta, }) + +export const clearWifiStatus = ( + robotName: string +): Types.ClearWifiStatusAction => ({ + type: Constants.CLEAR_WIFI_STATUS, + payload: { robotName }, +}) diff --git a/app/src/redux/networking/constants.ts b/app/src/redux/networking/constants.ts index 72c9c95b8f9..fc834452e67 100644 --- a/app/src/redux/networking/constants.ts +++ b/app/src/redux/networking/constants.ts @@ -103,3 +103,8 @@ export const POST_WIFI_DISCONNECT_SUCCESS: 'networking:POST_WIFI_DISCONNECT_SUCC export const POST_WIFI_DISCONNECT_FAILURE: 'networking:POST_WIFI_DISCONNECT_FAILURE' = 'networking:POST_WIFI_DISCONNECT_FAILURE' + +// clear wifi status + +export const CLEAR_WIFI_STATUS: 'networking:CLEAR_WIFI_STATUS' = + 'networking:CLEAR_WIFI_STATUS' diff --git a/app/src/redux/networking/reducer.ts b/app/src/redux/networking/reducer.ts index d0791e9ddb0..da3a67077bb 100644 --- a/app/src/redux/networking/reducer.ts +++ b/app/src/redux/networking/reducer.ts @@ -17,6 +17,21 @@ const getRobotState = ( robotName: string ): PerRobotNetworkingState => state[robotName] || INITIAL_ROBOT_STATE +const getWifiInterfaceKey = ( + networkingState: PerRobotNetworkingState +): string => { + const networkInterfacesEntries = Object.entries( + networkingState.interfaces ?? {} + ) + const wifiInterfaceEntry = networkInterfacesEntries.find( + networkInterface => networkInterface[1]?.type === Constants.INTERFACE_WIFI + ) + const wifiInterfaceKey = wifiInterfaceEntry?.[0] + + // default to 'mlan0' for type safety + return wifiInterfaceKey ?? 'mlan0' +} + export const networkingReducer: Reducer = ( state = INITIAL_STATE, action @@ -70,6 +85,28 @@ export const networkingReducer: Reducer = ( [robotName]: { ...robotState, eapOptions }, } } + + case Constants.CLEAR_WIFI_STATUS: { + const { robotName } = action.payload + const robotState = getRobotState(state, robotName) + const wifiInterfaceKey = getWifiInterfaceKey(robotState) + return { + ...state, + [robotName]: { + ...robotState, + interfaces: { + ...robotState.interfaces, + [wifiInterfaceKey]: { + ipAddress: null, + macAddress: 'unknown', + gatewayAddress: null, + state: 'disconnected', + type: Constants.INTERFACE_WIFI, + }, + }, + }, + } + } } return state diff --git a/app/src/redux/networking/types.ts b/app/src/redux/networking/types.ts index 54237eebb76..0077d8f29ba 100644 --- a/app/src/redux/networking/types.ts +++ b/app/src/redux/networking/types.ts @@ -149,6 +149,11 @@ export interface PostWifiDisconnectFailureAction { meta: RobotApiRequestMeta } +export interface ClearWifiStatusAction { + type: 'networking:CLEAR_WIFI_STATUS' + payload: { robotName: string } +} + export interface NetworkingDisconnectResponse { ssid: string } @@ -174,6 +179,7 @@ export type NetworkingAction = | PostWifiDisconnectAction | PostWifiDisconnectSuccessAction | PostWifiDisconnectFailureAction + | ClearWifiStatusAction // state types From 2fd0ecf79e22bdc983f212cef2fc5a2894c9ca9d Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Fri, 6 Oct 2023 17:03:02 -0400 Subject: [PATCH 38/79] feat(app): add loading state to LPC when applying offsets (#13702) closes RQA-1220 --- app/src/atoms/buttons/SmallButton.tsx | 4 +-- .../LabwarePositionCheckComponent.tsx | 16 ++++++++++- .../LabwarePositionCheck/ResultsSummary.tsx | 28 +++++++++++++++++-- .../__tests__/ResultsSummary.test.tsx | 18 ++++++++++++ .../organisms/LabwarePositionCheck/index.tsx | 1 + .../LabwarePositionCheck/useLaunchLPC.tsx | 6 +++- 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/app/src/atoms/buttons/SmallButton.tsx b/app/src/atoms/buttons/SmallButton.tsx index edf2c12fdce..5d97982aad3 100644 --- a/app/src/atoms/buttons/SmallButton.tsx +++ b/app/src/atoms/buttons/SmallButton.tsx @@ -30,8 +30,8 @@ interface SmallButtonProps extends StyleProps { onClick: React.MouseEventHandler buttonType?: SmallButtonTypes buttonText: React.ReactNode - iconPlacement?: IconPlacement - iconName?: IconName + iconPlacement?: IconPlacement | null + iconName?: IconName | null buttonCategory?: ButtonCategory // if not specified, it will be 'default' disabled?: boolean } diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx index 67be4d2a1bd..c6ba931821f 100644 --- a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -49,6 +49,7 @@ interface LabwarePositionCheckModalProps { protocolName: string caughtError?: Error setMaintenanceRunId: (id: string | null) => void + isDeletingMaintenanceRun: boolean } export const LabwarePositionCheckComponent = ( @@ -62,6 +63,7 @@ export const LabwarePositionCheckComponent = ( maintenanceRunId, setMaintenanceRunId, protocolName, + isDeletingMaintenanceRun, } = props const { t } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) @@ -102,6 +104,9 @@ export const LabwarePositionCheckComponent = ( ]) const [fatalError, setFatalError] = React.useState(null) + const [isApplyingOffsets, setIsApplyingOffsets] = React.useState( + false + ) const [ { workingOffsets, tipPickUpOffset }, registerPosition, @@ -295,12 +300,15 @@ export const LabwarePositionCheckComponent = ( } const handleApplyOffsets = (offsets: LabwareOffsetCreateData[]): void => { + setIsApplyingOffsets(true) Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) .then(() => { onCloseClick() + setIsApplyingOffsets(false) }) .catch((e: Error) => { setFatalError(`error applying labware offsets: ${e.message}`) + setIsApplyingOffsets(false) }) } @@ -356,7 +364,13 @@ export const LabwarePositionCheckComponent = ( ) } diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx index cb15c61b654..0356b71b083 100644 --- a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx @@ -28,6 +28,7 @@ import { MODULE_ICON_NAME_BY_TYPE, BORDERS, ALIGN_FLEX_END, + Icon, LocationIcon, } from '@opentrons/components' import { PythonLabwareOffsetSnippet } from '../../molecules/PythonLabwareOffsetSnippet' @@ -55,6 +56,8 @@ interface ResultsSummaryProps extends ResultsSummaryStep { workingOffsets: WorkingOffset[] existingOffsets: LabwareOffset[] handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void + isApplyingOffsets: boolean + isDeletingMaintenanceRun: boolean } export const ResultsSummary = ( props: ResultsSummaryProps @@ -65,10 +68,13 @@ export const ResultsSummary = ( workingOffsets, handleApplyOffsets, existingOffsets, + isApplyingOffsets, + isDeletingMaintenanceRun, } = props const labwareDefinitions = getLabwareDefinitionsFromCommands( protocolData.commands ) + const isSubmittingAndClosing = isApplyingOffsets || isDeletingMaintenanceRun const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn ) @@ -168,6 +174,9 @@ export const ResultsSummary = ( alignSelf={ALIGN_FLEX_END} onClick={() => handleApplyOffsets(offsetsToApply)} buttonText={i18n.format(t('apply_offsets'), 'capitalize')} + iconName={isSubmittingAndClosing ? 'ot-spinner' : null} + iconPlacement={isSubmittingAndClosing ? 'startIcon' : null} + disabled={isSubmittingAndClosing} /> ) : ( - handleApplyOffsets(offsetsToApply)}> - {i18n.format(t('apply_offsets'), 'capitalize')} + handleApplyOffsets(offsetsToApply)} + disabled={isSubmittingAndClosing} + > + + {isSubmittingAndClosing ? ( + + ) : null} + + {i18n.format(t('apply_offsets'), 'capitalize')} + + )} diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx index 26b9c355ffa..b48d4248c6f 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx @@ -33,6 +33,8 @@ describe('ResultsSummary', () => { protocolData: mockCompletedAnalysis, workingOffsets: mockWorkingOffsets, existingOffsets: mockExistingOffsets, + isApplyingOffsets: false, + isDeletingMaintenanceRun: false, handleApplyOffsets: jest.fn(), } }) @@ -54,6 +56,22 @@ describe('ResultsSummary', () => { getByRole('button', { name: 'Apply offsets' }).click() expect(props.handleApplyOffsets).toHaveBeenCalled() }) + it('does disables the CTA to apply offsets when offsets are already being applied', () => { + props.isApplyingOffsets = true + const { getByRole } = render(props) + const button = getByRole('button', { name: 'Apply offsets' }) + expect(button).toBeDisabled() + button.click() + expect(props.handleApplyOffsets).not.toHaveBeenCalled() + }) + it('does disables the CTA to apply offsets when the maintenance run is being deleted', () => { + props.isDeletingMaintenanceRun = true + const { getByRole } = render(props) + const button = getByRole('button', { name: 'Apply offsets' }) + expect(button).toBeDisabled() + button.click() + expect(props.handleApplyOffsets).not.toHaveBeenCalled() + }) it('renders a row per offset to apply', () => { const { getByRole, queryAllByRole } = render(props) expect( diff --git a/app/src/organisms/LabwarePositionCheck/index.tsx b/app/src/organisms/LabwarePositionCheck/index.tsx index 084384321b3..a37740aeab0 100644 --- a/app/src/organisms/LabwarePositionCheck/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/index.tsx @@ -15,6 +15,7 @@ interface LabwarePositionCheckModalProps { protocolName: string caughtError?: Error setMaintenanceRunId: (id: string | null) => void + isDeletingMaintenanceRun: boolean } // We explicitly wrap LabwarePositionCheckComponent in an ErrorBoundary because an error might occur while pulling in diff --git a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx index 0afe0df30f5..55462fc579d 100644 --- a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx +++ b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx @@ -18,7 +18,10 @@ export function useLaunchLPC( const { createTargetedMaintenanceRun, } = useCreateTargetedMaintenanceRunMutation() - const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation() + const { + deleteMaintenanceRun, + isLoading: isDeletingMaintenanceRun, + } = useDeleteMaintenanceRunMutation() const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) const [maintenanceRunId, setMaintenanceRunId] = React.useState( null @@ -74,6 +77,7 @@ export function useLaunchLPC( maintenanceRunId={maintenanceRunId} setMaintenanceRunId={setMaintenanceRunId} protocolName={protocolName ?? ''} + isDeletingMaintenanceRun={isDeletingMaintenanceRun} /> ) : null, } From 93420346b9a326d8bbf47f87645a559da5414cff Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 9 Oct 2023 11:18:00 -0400 Subject: [PATCH 39/79] fix(aoo): fix lint-js error (#13742) * fix(app): fix lint-js error --- app/src/App/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index bc574975776..77e52119ca2 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -97,7 +97,7 @@ export function useProtocolReceiptToast(): void { { closeButton: true, disableTimeout: true, - displayType: 'odd' + displayType: 'odd', } ) }) From 11b35ed6f9cf964460d860494d744c9733d5a7b2 Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Mon, 9 Oct 2023 15:22:09 -0400 Subject: [PATCH 40/79] feat(shared-data): add custom & default thermocycler overlap offsets for labware (#13630) * added TC overlaps for nest & biorad pcr plates * added a backwards-compatible default overlap for TC labware on OT2 * fix pd e2e migration fixture --------- Co-authored-by: Jethary --- .../protocol_engine/state/labware.py | 17 +++- .../state/test_labware_view.py | 92 ++++++++++++++++--- .../fixtures/protocol/7/doItAllV7.json | 7 ++ .../2/biorad_96_wellplate_200ul_pcr/2.json | 7 ++ .../2.json | 7 ++ 5 files changed, 118 insertions(+), 12 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index b567eaa2f5c..c56edf4de5f 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -560,12 +560,27 @@ def get_module_overlap_offsets( """Get the labware's overlap with requested module model.""" definition = self.get_definition(labware_id) stacking_overlap = definition.stackingOffsetWithModule.get( - str(module_model.value), OverlapOffset(x=0, y=0, z=0) + str(module_model.value) ) + if not stacking_overlap: + if self._is_thermocycler_on_ot2(module_model): + return OverlapOffset(x=0, y=0, z=10.7) + else: + return OverlapOffset(x=0, y=0, z=0) + return OverlapOffset( x=stacking_overlap.x, y=stacking_overlap.y, z=stacking_overlap.z ) + def _is_thermocycler_on_ot2(self, module_model: ModuleModel) -> bool: + """Whether the given module is a thermocycler with the current deck being an OT2 deck.""" + robot_model = self.get_deck_definition()["robot"]["model"] + return ( + module_model + in [ModuleModel.THERMOCYCLER_MODULE_V1, ModuleModel.THERMOCYCLER_MODULE_V2] + and robot_model == "OT-2 Standard" + ) + def get_default_magnet_height(self, module_id: str, offset: float) -> float: """Return a labware's default Magnetic Module engage height with added offset, if supplied. diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index 7d277d93b5a..6de6ba0d191 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -1,9 +1,10 @@ """Labware state store tests.""" import pytest from datetime import datetime -from typing import Dict, Optional, cast, ContextManager, Any, Union +from typing import Dict, Optional, cast, ContextManager, Any, Union, NamedTuple, List from contextlib import nullcontext as does_not_raise +from opentrons_shared_data.deck import load as load_deck from opentrons_shared_data.deck.dev_types import DeckDefinitionV3 from opentrons_shared_data.pipette.dev_types import LabwareUri from opentrons_shared_data.labware.labware_definition import ( @@ -13,6 +14,11 @@ GripperOffsets, OffsetVector, ) + +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT2_DECK, + STANDARD_OT3_DECK, +) from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName, Point, MountType @@ -39,7 +45,6 @@ LabwareLoadParams, ) - plate = LoadedLabware( id="plate-id", loadName="plate-load-name", @@ -686,26 +691,91 @@ def test_get_labware_overlap_offsets() -> None: assert result == OverlapOffset(x=1, y=2, z=3) -def test_get_module_overlap_offsets() -> None: +class ModuleOverlapSpec(NamedTuple): + """Spec data to test LabwareView.get_module_overlap_offsets.""" + + spec_deck_definition: DeckDefinitionV3 + module_model: ModuleModel + stacking_offset_with_module: Dict[str, SharedDataOverlapOffset] + expected_offset: OverlapOffset + + +module_overlap_specs: List[ModuleOverlapSpec] = [ + ModuleOverlapSpec( + # Labware on temp module on OT2, with stacking overlap for temp module + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 3), + module_model=ModuleModel.TEMPERATURE_MODULE_V2, + stacking_offset_with_module={ + str(ModuleModel.TEMPERATURE_MODULE_V2.value): SharedDataOverlapOffset( + x=1, y=2, z=3 + ), + }, + expected_offset=OverlapOffset(x=1, y=2, z=3), + ), + ModuleOverlapSpec( + # Labware on TC Gen1 on OT2, with stacking overlap for TC Gen1 + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 3), + module_model=ModuleModel.THERMOCYCLER_MODULE_V1, + stacking_offset_with_module={ + str(ModuleModel.THERMOCYCLER_MODULE_V1.value): SharedDataOverlapOffset( + x=11, y=22, z=33 + ), + }, + expected_offset=OverlapOffset(x=11, y=22, z=33), + ), + ModuleOverlapSpec( + # Labware on TC Gen2 on OT2, with no stacking overlap + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 3), + module_model=ModuleModel.THERMOCYCLER_MODULE_V2, + stacking_offset_with_module={}, + expected_offset=OverlapOffset(x=0, y=0, z=10.7), + ), + ModuleOverlapSpec( + # Labware on TC Gen2 on Flex, with no stacking overlap + spec_deck_definition=load_deck(STANDARD_OT3_DECK, 3), + module_model=ModuleModel.THERMOCYCLER_MODULE_V2, + stacking_offset_with_module={}, + expected_offset=OverlapOffset(x=0, y=0, z=0), + ), + ModuleOverlapSpec( + # Labware on TC Gen2 on Flex, with stacking overlap for TC Gen2 + spec_deck_definition=load_deck(STANDARD_OT3_DECK, 3), + module_model=ModuleModel.THERMOCYCLER_MODULE_V2, + stacking_offset_with_module={ + str(ModuleModel.THERMOCYCLER_MODULE_V2.value): SharedDataOverlapOffset( + x=111, y=222, z=333 + ), + }, + expected_offset=OverlapOffset(x=111, y=222, z=333), + ), +] + + +@pytest.mark.parametrize( + argnames=ModuleOverlapSpec._fields, + argvalues=module_overlap_specs, +) +def test_get_module_overlap_offsets( + spec_deck_definition: DeckDefinitionV3, + module_model: ModuleModel, + stacking_offset_with_module: Dict[str, SharedDataOverlapOffset], + expected_offset: OverlapOffset, +) -> None: """It should get the labware overlap offsets.""" subject = get_labware_view( + deck_definition=spec_deck_definition, labware_by_id={"plate-id": plate}, definitions_by_uri={ "some-plate-uri": LabwareDefinition.construct( # type: ignore[call-arg] - stackingOffsetWithModule={ - str( - ModuleModel.TEMPERATURE_MODULE_V2.value - ): SharedDataOverlapOffset(x=1, y=2, z=3) - } + stackingOffsetWithModule=stacking_offset_with_module ) }, ) - result = subject.get_module_overlap_offsets( - labware_id="plate-id", module_model=ModuleModel.TEMPERATURE_MODULE_V2 + labware_id="plate-id", module_model=module_model ) - assert result == OverlapOffset(x=1, y=2, z=3) + assert result == expected_offset def test_get_default_magnet_height( diff --git a/protocol-designer/fixtures/protocol/7/doItAllV7.json b/protocol-designer/fixtures/protocol/7/doItAllV7.json index a2c759c5437..0fdbfd3b430 100644 --- a/protocol-designer/fixtures/protocol/7/doItAllV7.json +++ b/protocol-designer/fixtures/protocol/7/doItAllV7.json @@ -2415,6 +2415,13 @@ "stackingOffsetWithLabware": { "opentrons_96_pcr_adapter": { "x": 0, "y": 0, "z": 10.2 }, "opentrons_96_well_aluminum_block": { "x": 0, "y": 0, "z": 12.66 } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } } }, "opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1": { diff --git a/shared-data/labware/definitions/2/biorad_96_wellplate_200ul_pcr/2.json b/shared-data/labware/definitions/2/biorad_96_wellplate_200ul_pcr/2.json index a33a8a14d6c..b15f38bfc94 100644 --- a/shared-data/labware/definitions/2/biorad_96_wellplate_200ul_pcr/2.json +++ b/shared-data/labware/definitions/2/biorad_96_wellplate_200ul_pcr/2.json @@ -1025,5 +1025,12 @@ "y": 0, "z": 15.41 } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.75 + } } } diff --git a/shared-data/labware/definitions/2/nest_96_wellplate_100ul_pcr_full_skirt/2.json b/shared-data/labware/definitions/2/nest_96_wellplate_100ul_pcr_full_skirt/2.json index cc66043024a..5aba73ae4a4 100644 --- a/shared-data/labware/definitions/2/nest_96_wellplate_100ul_pcr_full_skirt/2.json +++ b/shared-data/labware/definitions/2/nest_96_wellplate_100ul_pcr_full_skirt/2.json @@ -1028,5 +1028,12 @@ "y": 0, "z": 12.66 } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } } } From 9998948235df063f05ad8a642f48246bdc20d4f6 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 9 Oct 2023 18:09:05 -0400 Subject: [PATCH 41/79] fix(app, components): fix module calibration slot selection rendering for tc (#13741) * fix(app): fix module calibration slot selection rendering for tc --- .../ModuleWizardFlows/SelectLocation.tsx | 4 ++ .../src/hooks/useSelectDeckLocation/index.tsx | 47 +++++++++++++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index 6cd18622e6d..eb78f2b63aa 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -6,6 +6,7 @@ import { ModuleLocation, getDeckDefFromRobotType, getModuleDisplayName, + THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import { RESPONSIVENESS, @@ -72,6 +73,9 @@ export const SelectLocation = ( return acc return [...acc, { slotName: slot.id }] }, [])} + isThermocycler={ + attachedModule.moduleType === THERMOCYCLER_MODULE_TYPE + } /> } bodyText={bodyText} diff --git a/components/src/hooks/useSelectDeckLocation/index.tsx b/components/src/hooks/useSelectDeckLocation/index.tsx index dd89082aa90..c6b2f783870 100644 --- a/components/src/hooks/useSelectDeckLocation/index.tsx +++ b/components/src/hooks/useSelectDeckLocation/index.tsx @@ -11,7 +11,10 @@ import { Text } from '../../primitives' import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../styles' import { DeckSlotLocation } from '../../hardware-sim/DeckSlotLocation' -const X_CROP_MM = 60 +const X_CROP_MM = 0 +const X_ADJUSTMENT_FOR_TC = '-50' +const Y_ADJUSTMENT_FOR_TC = '214' + export function useDeckLocationSelect( robotType: RobotType ): { DeckLocationSelect: JSX.Element; selectedLocation: ModuleLocation } { @@ -37,12 +40,14 @@ interface DeckLocationSelectProps { selectedLocation: ModuleLocation setSelectedLocation: (loc: ModuleLocation) => void disabledLocations?: ModuleLocation[] + isThermocycler?: boolean } export function DeckLocationSelect({ deckDef, selectedLocation, setSelectedLocation, disabledLocations = [], + isThermocycler = false, }: DeckLocationSelectProps): JSX.Element { return ( + + + + + Selected + + + + ) + } else if (slot.id === 'A1' && isThermocycler) { + return null + } return ( @@ -100,3 +123,11 @@ export function DeckLocationSelect({ ) } + +const INNER_DIV_PROPS = { + display: 'flex', + alignItems: ALIGN_CENTER, + justifyContent: JUSTIFY_CENTER, + height: '100%', + gridGap: SPACING.spacing4, +} From 6b2b70e359ef489bb0815b863bcfe95d07fd547d Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Tue, 10 Oct 2023 11:44:43 -0400 Subject: [PATCH 42/79] feat(api): add AuthorizedKeys reset option for the OT-2 + from_local fixes (#13745) * Add error handling so we don't stop processing all keys if one fails * Skip hidden files when searching for public keys * Accept ECDSA keys as well * Don't add existing keys to the authorized_keys file * Report that keys were added if they are valid even if they already exist on the bot, so the client gets feedback on the operation. --- api/src/opentrons/config/reset.py | 1 + .../test_settings_reset_options.tavern.yaml | 30 +++++++++++++++++++ .../service/legacy/routers/test_settings.py | 5 ++++ .../otupdate/common/ssh_key_management.py | 25 +++++++++------- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/api/src/opentrons/config/reset.py b/api/src/opentrons/config/reset.py index ccba3b2fdcb..42a2b606106 100644 --- a/api/src/opentrons/config/reset.py +++ b/api/src/opentrons/config/reset.py @@ -49,6 +49,7 @@ class ResetOptionId(str, Enum): ResetOptionId.pipette_offset, ResetOptionId.tip_length_calibrations, ResetOptionId.runs_history, + ResetOptionId.authorized_keys, ] _FLEX_RESET_OPTIONS = [ ResetOptionId.boot_scripts, diff --git a/robot-server/tests/integration/test_settings_reset_options.tavern.yaml b/robot-server/tests/integration/test_settings_reset_options.tavern.yaml index 7dc49479bba..4025206afee 100644 --- a/robot-server/tests/integration/test_settings_reset_options.tavern.yaml +++ b/robot-server/tests/integration/test_settings_reset_options.tavern.yaml @@ -27,6 +27,10 @@ stages: - id: runsHistory name: Clear Runs History description: !re_search 'Erase this device''s stored history of protocols and runs.' + - id: authorizedKeys + name: SSH Authorized Keys + description: !re_search 'Clear the ssh authorized keys' + --- test_name: POST Reset bootScripts option marks: @@ -123,6 +127,32 @@ stages: message: "gripperOffsetCalibrations is not a valid reset option." errorCode: "4000" --- +test_name: POST Reset authorizedKeys option +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: POST Reset authorizedKeys true + request: + url: '{ot2_server_base_url}/settings/reset' + method: POST + json: + authorizedKeys: true + response: + status_code: 200 + json: + message: "Options 'authorized_keys' were reset" + - name: POST Reset authorizedKeys false + request: + url: '{ot2_server_base_url}/settings/reset' + method: POST + json: + authorizedKeys: false + response: + status_code: 200 + json: + message: 'Nothing to do' +--- test_name: POST Reset non existant option marks: - usefixtures: diff --git a/robot-server/tests/service/legacy/routers/test_settings.py b/robot-server/tests/service/legacy/routers/test_settings.py index 0f96946dac0..05f4a187045 100644 --- a/robot-server/tests/service/legacy/routers/test_settings.py +++ b/robot-server/tests/service/legacy/routers/test_settings.py @@ -514,6 +514,7 @@ def test_available_resets(api_client): "bootScripts", "tipLengthCalibrations", "runsHistory", + "authorizedKeys", ] ) == sorted([item["id"] for item in options_list]) @@ -551,6 +552,7 @@ async def mock_get_persistence_resetter() -> PersistenceResetter: "pipetteOffsetCalibrations": False, "tipLengthCalibrations": False, "runsHistory": False, + "authorizedKeys": False, }, set(), ], @@ -562,6 +564,7 @@ async def mock_get_persistence_resetter() -> PersistenceResetter: "tipLengthCalibrations": True, "deckCalibration": True, "runsHistory": True, + "authorizedKeys": True, # TODO(mm, 2023-08-04): Figure out how to test Flex-only options, # then add gripperOffsetCalibrations and onDeviceDisplay. }, @@ -575,8 +578,10 @@ async def mock_get_persistence_resetter() -> PersistenceResetter: # mark_directory_reset() being an async method, and api_client having # its own event loop that interferes with making this test async. ResetOptionId.runs_history, + ResetOptionId.authorized_keys, }, ], + [{"authorizedKeys": True}, {ResetOptionId.authorized_keys}], [{"bootScripts": True}, {ResetOptionId.boot_scripts}], [{"pipetteOffsetCalibrations": True}, {ResetOptionId.pipette_offset}], [{"tipLengthCalibrations": True}, {ResetOptionId.tip_length_calibrations}], diff --git a/update-server/otupdate/common/ssh_key_management.py b/update-server/otupdate/common/ssh_key_management.py index 8129322b80d..fa327806701 100644 --- a/update-server/otupdate/common/ssh_key_management.py +++ b/update-server/otupdate/common/ssh_key_management.py @@ -251,7 +251,8 @@ async def add_from_local(request: web.Request) -> web.Response: Path(root, file) for root, _, files in os.walk("/media") for file in files - if file.endswith(".pub") + # skip hidden files + if not file.startswith(".") and file.endswith(".pub") ] if not pub_keys: LOG.warning("No keys found") @@ -265,16 +266,20 @@ async def add_from_local(request: web.Request) -> web.Response: new_keys = list() with open(AUTHORIZED_KEYS, "a") as fh: for key in pub_keys: - with open(key, "r") as gh: - ssh_key = gh.read() - if "ssh-rsa" not in ssh_key: - LOG.warning(f"Invalid ssh public key: {key}") - continue - key_hash = hashlib.new("md5", ssh_key.encode()).hexdigest() - if not key_present(key_hash): - fh.write(f"{ssh_key}\n") - LOG.info(f"Added new rsa key: {key}") + try: + with open(key, "r") as gh: + ssh_key = gh.read().strip() + if "ssh-rsa" not in ssh_key and "ecdsa" not in ssh_key: + LOG.warning(f"Invalid ssh public key: {key}") + continue + key_hash = hashlib.new("md5", ssh_key.encode()).hexdigest() + if not key_present(key_hash): + fh.write(f"{ssh_key}\n") + LOG.info(f"Added new rsa key: {key}") new_keys.append(key_hash) + except Exception as e: + LOG.error(f"Could not process ssh public key: {key} {e}") + continue return web.json_response( # type: ignore[no-untyped-call,no-any-return] data={"message": f"Added {len(new_keys)} new keys", "key_md5": new_keys}, From 47d486c3ce30e5ee4cca63abd8200d55f83c53c7 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 10 Oct 2023 12:07:25 -0400 Subject: [PATCH 43/79] refactor(api): Monitor PAPI commands, not PE commands (#13724) * Remove broker from ProtocolEngine. This partially reverts commit 24eb3c1f135a2736a65f478620180a9026508d9b ("fix(api): New protocols print a JSON "run log" from opentrons_execute and opentrons.execute.execute() (#13629)"). * Add broker to ProtocolRunner. * Update execute.py. * Update test_execute.py. * Further deduplicate test_execute.py. --- api/src/opentrons/execute.py | 40 ++----- .../protocol_engine/command_monitor.py | 65 ----------- .../protocol_engine/protocol_engine.py | 32 ------ .../protocol_engine/state/change_notifier.py | 12 --- .../protocol_engine/state/commands.py | 6 +- .../opentrons/protocol_engine/state/state.py | 12 --- .../protocol_runner/protocol_runner.py | 23 +++- .../state/test_change_notifier.py | 17 --- .../state/test_command_monitor.py | 92 ---------------- .../state/test_command_view.py | 9 -- api/tests/opentrons/test_execute.py | 101 +++++------------- 11 files changed, 55 insertions(+), 354 deletions(-) delete mode 100644 api/src/opentrons/protocol_engine/command_monitor.py delete mode 100644 api/tests/opentrons/protocol_engine/state/test_command_monitor.py diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index 709cd2c6c69..2f4a7fdc9e9 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -62,7 +62,6 @@ DeckType, EngineStatus, ErrorOccurrence as ProtocolEngineErrorOccurrence, - command_monitor as pe_command_monitor, create_protocol_engine, create_protocol_engine_in_thread, ) @@ -641,10 +640,6 @@ def _run_file_pe( ) -> None: """Run a protocol file with Protocol Engine.""" - def send_command_to_emit_runlog(event: pe_command_monitor.Event) -> None: - if emit_runlog is not None: - emit_runlog(_adapt_command(event)) - async def run(protocol_source: ProtocolSource) -> None: protocol_engine = await create_protocol_engine( hardware_api=hardware_api, @@ -657,12 +652,15 @@ async def run(protocol_source: ProtocolSource) -> None: hardware_api=hardware_api, ) - with pe_command_monitor.monitor_commands( - protocol_engine, callback=send_command_to_emit_runlog - ): + unsubscribe = protocol_runner.broker.subscribe( + "command", lambda event: emit_runlog(event) if emit_runlog else None + ) + try: # TODO(mm, 2023-06-30): This will home and drop tips at the end, which is not how # things have historically behaved with PAPIv2.13 and older or JSONv5 and older. result = await protocol_runner.run(protocol_source) + finally: + unsubscribe() if result.state_summary.status != EngineStatus.SUCCEEDED: raise _ProtocolEngineExecuteError(result.state_summary.errors) @@ -735,32 +733,6 @@ def _adapt_protocol_source( yield protocol_source -def _adapt_command(event: pe_command_monitor.Event) -> command_types.CommandMessage: - """Convert a Protocol Engine command event to an old-school command_types.CommandMesage.""" - before_or_after: command_types.MessageSequenceId = ( - "before" if isinstance(event, pe_command_monitor.RunningEvent) else "after" - ) - - message: command_types.CommentMessage = { - # TODO(mm, 2023-09-26): If we can without breaking the public API, remove the requirement - # to supply a "name" here. If we can't do that, consider adding a special name value - # so we don't have to lie and call every command a comment. - "name": "command.COMMENT", - "id": event.command.id, - "$": before_or_after, - # TODO(mm, 2023-09-26): Convert this machine-readable JSON into a human-readable message - # to match behavior from before Protocol Engine. - # https://opentrons.atlassian.net/browse/RSS-320 - "payload": {"text": event.command.json()}, - # As far as I know, "error" is not part of the public-facing API, so it doesn't matter - # what we put here. Leaving it as `None` to avoid difficulties in converting between - # the Protocol Engine `ErrorOccurrence` model and the regular Python `Exception` type - # that this field expects. - "error": None, - } - return message - - def _get_global_hardware_controller(robot_type: RobotType) -> ThreadManagedHardware: # Build a hardware controller in a worker thread, which is necessary # because ipython runs its notebook in asyncio but the notebook diff --git a/api/src/opentrons/protocol_engine/command_monitor.py b/api/src/opentrons/protocol_engine/command_monitor.py deleted file mode 100644 index 9f2985f59b0..00000000000 --- a/api/src/opentrons/protocol_engine/command_monitor.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Monitor the execution of commands in a `ProtocolEngine`.""" - - -from dataclasses import dataclass -import typing -import contextlib - - -from opentrons.protocol_engine import Command, ProtocolEngine - - -@dataclass -class RunningEvent: - """Emitted when a command starts running.""" - - command: Command - - -@dataclass -class NoLongerRunningEvent: - """Emitted when a command stops running--either because it succeeded, or failed.""" - - command: Command - - -Event = typing.Union[RunningEvent, NoLongerRunningEvent] -Callback = typing.Callable[[Event], None] - - -@contextlib.contextmanager -def monitor_commands( - protocol_engine: ProtocolEngine, - callback: Callback, -) -> typing.Generator[None, None, None]: - """Monitor the execution of commands in `protocol_engine`. - - While this context manager is open, `callback` will be called any time `protocol_engine` - starts or stops a command. - """ - # Subscribe to all state updates in protocol_engine. - # On every update, diff the new state against the last state and see if the currently - # running command has changed. If it has, emit the appropriate events. - - last_running_id: typing.Optional[str] = None - - def handle_state_update(_message_from_broker: None) -> None: - nonlocal last_running_id - - running_id = protocol_engine.state_view.commands.get_running() - if running_id != last_running_id: - if last_running_id is not None: - callback( - NoLongerRunningEvent( - protocol_engine.state_view.commands.get(last_running_id) - ) - ) - - if running_id is not None: - callback( - RunningEvent(protocol_engine.state_view.commands.get(running_id)) - ) - last_running_id = running_id - - with protocol_engine.state_update_broker.subscribed(handle_state_update): - yield diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 9ad7cb3a27c..207ebddd9d8 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -12,8 +12,6 @@ EnumeratedError, ) -from opentrons.util.broker import ReadOnlyBroker - from .errors import ProtocolCommandFailedError, ErrorOccurrence from .errors.exceptions import EStopActivatedError from . import commands, slot_standardization @@ -131,36 +129,6 @@ def state_view(self) -> StateView: """Get an interface to retrieve calculated state values.""" return self._state_store - @property - def state_update_broker(self) -> ReadOnlyBroker[None]: - """Return a broker that you can use to get notified of all state updates. - - For example, you can use this to do something any time a new command starts running. - - `ProtocolEngine` will publish a message to this broker (with the placeholder value `None`) - any time its state updates. Then, when you receive that message, you can get the latest - state through `state_view` and inspect it to see whether something happened that you care - about. - - Warning: - Use this mechanism sparingly, because it has several footguns: - - * Your callbacks will run synchronously, on every state update. - If they take a long time, they will harm analysis and run speed. - - * Your callbacks will run in the thread and asyncio event loop that own this - `ProtocolEngine`. (See the concurrency notes in the `ProtocolEngine` docstring.) - If your callbacks interact with things in other threads or event loops, - take appropriate precautions to keep them concurrency-safe. - - * Currently, if your callback raises an exception, it will propagate into - `ProtocolEngine` and be treated like any other internal error. This will probably - stop the run. If you expect your code to raise exceptions and don't want - that to happen, consider catching and logging them at the top level of your callback, - before they propagate into `ProtocolEngine`. - """ - return self._state_store.update_broker - def add_plugin(self, plugin: AbstractPlugin) -> None: """Add a plugin to the engine to customize behavior.""" self._plugin_starter.start(plugin) diff --git a/api/src/opentrons/protocol_engine/state/change_notifier.py b/api/src/opentrons/protocol_engine/state/change_notifier.py index 629cb89f368..3c72f277913 100644 --- a/api/src/opentrons/protocol_engine/state/change_notifier.py +++ b/api/src/opentrons/protocol_engine/state/change_notifier.py @@ -1,8 +1,6 @@ """Simple state change notification interface.""" import asyncio -from opentrons.util.broker import Broker, ReadOnlyBroker - class ChangeNotifier: """An interface tto emit or subscribe to state change notifications.""" @@ -10,22 +8,12 @@ class ChangeNotifier: def __init__(self) -> None: """Initialize the ChangeNotifier with an internal Event.""" self._event = asyncio.Event() - self._broker = Broker[None]() def notify(self) -> None: """Notify all `wait`'ers that the state has changed.""" self._event.set() - self._broker.publish(None) async def wait(self) -> None: """Wait until the next state change notification.""" self._event.clear() await self._event.wait() - - @property - def broker(self) -> ReadOnlyBroker[None]: - """Return a broker that you can use to get notified of all changes. - - This is an alternative interface to `wait()`. - """ - return self._broker diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index ae0ba7898cb..b4301c22920 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -507,17 +507,13 @@ def get_error(self) -> Optional[ErrorOccurrence]: else: return run_error or finish_error - def get_running(self) -> Optional[str]: - """Return the ID of the command that's currently running, if any.""" - return self._state.running_command_id - def get_current(self) -> Optional[CurrentCommand]: """Return the "current" command, if any. The "current" command is the command that is currently executing, or the most recent command to have completed. """ - if self._state.running_command_id is not None: + if self._state.running_command_id: entry = self._state.commands_by_id[self._state.running_command_id] return CurrentCommand( command_id=entry.command.id, diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 5fdbd394b32..7e4695e15e6 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -8,7 +8,6 @@ from opentrons_shared_data.deck.dev_types import DeckDefinitionV3 from opentrons.protocol_engine.types import ModuleOffsetData -from opentrons.util.broker import ReadOnlyBroker from ..resources import DeckFixedLabware from ..actions import Action, ActionHandler @@ -240,17 +239,6 @@ async def wait_for( return is_done - # We return ReadOnlyBroker[None] instead of ReadOnlyBroker[StateView] in order to avoid - # confusion with state mutability. If a caller needs to know the new state, they can - # retrieve it explicitly with `ProtocolEngine.state_view`. - @property - def update_broker(self) -> ReadOnlyBroker[None]: - """Return a broker that you can use to get notified of all state updates. - - This is an alternative interface to `wait_for()`. - """ - return self._change_notifier.broker - def _get_next_state(self) -> State: """Get a new instance of the state value object.""" return State( diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index 1b49a159087..5aabd878ca3 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -59,6 +59,21 @@ class AbstractRunner(ABC): def __init__(self, protocol_engine: ProtocolEngine) -> None: self._protocol_engine = protocol_engine + self._broker = LegacyBroker() + + # TODO(mm, 2023-10-03): `LegacyBroker` is specific to Python protocols and JSON protocols ≤v5. + # We'll need to extend this in order to report progress from newer JSON protocols. + # + # TODO(mm, 2023-10-04): When we switch this to return a new `Broker` instead of a + # `LegacyBroker`, we should annotate the return type as a `ReadOnlyBroker`. + @property + def broker(self) -> LegacyBroker: + """Return a broker that you can subscribe to in order to monitor protocol progress. + + Currently, this only returns messages for `PythonAndLegacyRunner`. + Otherwise, it's a no-op. + """ + return self._broker def was_started(self) -> bool: """Whether the run has been started. @@ -136,20 +151,20 @@ async def load( protocol = self._legacy_file_reader.read( protocol_source, labware_definitions, python_parse_mode ) - broker = None equipment_broker = None if protocol.api_level < LEGACY_PYTHON_API_VERSION_CUTOFF: - broker = LegacyBroker() equipment_broker = Broker[LegacyLoadInfo]() self._protocol_engine.add_plugin( - LegacyContextPlugin(broker=broker, equipment_broker=equipment_broker) + LegacyContextPlugin( + broker=self._broker, equipment_broker=equipment_broker + ) ) context = self._legacy_context_creator.create( protocol=protocol, - broker=broker, + broker=self._broker, equipment_broker=equipment_broker, ) initial_home_command = pe_commands.HomeCreate( diff --git a/api/tests/opentrons/protocol_engine/state/test_change_notifier.py b/api/tests/opentrons/protocol_engine/state/test_change_notifier.py index ec62362d6da..4967e6d254e 100644 --- a/api/tests/opentrons/protocol_engine/state/test_change_notifier.py +++ b/api/tests/opentrons/protocol_engine/state/test_change_notifier.py @@ -54,20 +54,3 @@ async def _do_task_3() -> None: await asyncio.gather(task_1, task_2, task_3) assert results == [1, 2, 3] - - -async def test_broker() -> None: - """Test that notifications are available synchronously through `ChangeNotifier.broker`.""" - notify_count = 5 - - subject = ChangeNotifier() - received = 0 - - def callback(_message_from_broker: None) -> None: - nonlocal received - received += 1 - - with subject.broker.subscribed(callback): - for notify_number in range(notify_count): - subject.notify() - assert received == notify_number + 1 diff --git a/api/tests/opentrons/protocol_engine/state/test_command_monitor.py b/api/tests/opentrons/protocol_engine/state/test_command_monitor.py deleted file mode 100644 index dec820a97f6..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_command_monitor.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Unit tests for `opentrons.protocol_engine.command_monitor`.""" - - -from datetime import datetime -from typing import List - -from decoy import Decoy - -from opentrons.protocol_engine import CommandStatus, ProtocolEngine, commands -from opentrons.protocol_engine.command_monitor import ( - Event, - NoLongerRunningEvent, - RunningEvent, - monitor_commands as subject, -) -from opentrons.util.broker import Broker - - -def _make_dummy_command(id: str, completed: bool) -> commands.Command: - if completed: - return commands.Comment( - id=id, - key=id, - status=CommandStatus.SUCCEEDED, - createdAt=datetime(2023, 9, 26), - params=commands.CommentParams(message=""), - result=None, - ) - else: - return commands.Comment( - id=id, - key=id, - status=CommandStatus.RUNNING, - createdAt=datetime(2023, 9, 26), - completedAt=datetime(2023, 9, 26), - params=commands.CommentParams(message=""), - result=commands.CommentResult(), - ) - - -def test_monitor_commands(decoy: Decoy) -> None: - """Test that it translates state updates into command running/no-longer-running events.""" - mock_protocol_engine = decoy.mock(cls=ProtocolEngine) - mock_command_view = mock_protocol_engine.state_view.commands - state_update_broker = Broker[None]() - decoy.when(mock_protocol_engine.state_update_broker).then_return( - state_update_broker - ) - - command_1_running = _make_dummy_command(id="command-1", completed=False) - command_1_completed = _make_dummy_command(id="command-1", completed=True) - command_2_running = _make_dummy_command(id="command-2", completed=False) - command_2_completed = _make_dummy_command(id="command-2", completed=True) - - received_events: List[Event] = [] - - def callback(event: Event) -> None: - received_events.append(event) - - with subject(mock_protocol_engine, callback): - # Feed the subject these states, in sequence: - # 1. No command running - # 2. "command-1" running - # 3. "command-2" running - # 4. No command running - # Between each state, notify the subject by publishing a message to the broker that it's - # subscribed to. - - decoy.when(mock_command_view.get_running()).then_return(None) - state_update_broker.publish(message=None) - - decoy.when(mock_command_view.get_running()).then_return("command-1") - decoy.when(mock_command_view.get("command-1")).then_return(command_1_running) - state_update_broker.publish(message=None) - - decoy.when(mock_command_view.get_running()).then_return("command-2") - decoy.when(mock_command_view.get("command-1")).then_return(command_1_completed) - decoy.when(mock_command_view.get("command-2")).then_return(command_2_running) - state_update_broker.publish(message=None) - - decoy.when(mock_command_view.get_running()).then_return(None) - decoy.when(mock_command_view.get("command-2")).then_return(command_2_completed) - state_update_broker.publish(message=None) - - # Make sure the callback converted the sequence of state updates into the expected sequence - # of events. - assert received_events == [ - RunningEvent(command_1_running), - NoLongerRunningEvent(command_1_completed), - RunningEvent(command_2_running), - NoLongerRunningEvent(command_2_completed), - ] diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view.py index d4f77db8dbe..b9cc6835ce3 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view.py @@ -660,15 +660,6 @@ def test_get_okay_to_clear(subject: CommandView, expected_is_okay: bool) -> None assert subject.get_is_okay_to_clear() is expected_is_okay -def test_get_running() -> None: - """It should return the command that's currently running.""" - subject = get_command_view(running_command_id=None) - assert subject.get_running() is None - - subject = get_command_view(running_command_id="command-id") - assert subject.get_running() == "command-id" - - def test_get_current() -> None: """It should return the "current" command.""" subject = get_command_view( diff --git a/api/tests/opentrons/test_execute.py b/api/tests/opentrons/test_execute.py index d233914af24..9a4ac9fb673 100644 --- a/api/tests/opentrons/test_execute.py +++ b/api/tests/opentrons/test_execute.py @@ -6,7 +6,7 @@ import textwrap import mock from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Generator, TextIO, cast +from typing import TYPE_CHECKING, Any, Callable, Generator, List, TextIO, cast import pytest @@ -58,60 +58,35 @@ async def dummy_delay(self: Any, duration_s: float) -> None: return gai_mock -@pytest.mark.parametrize("protocol_file", ["testosaur_v2.py"]) -def test_execute_function_apiv2( - protocol: Protocol, - protocol_file: str, - virtual_smoothie_env: None, - mock_get_attached_instr: mock.AsyncMock, -) -> None: - """Test `execute()` with a Python file.""" - converted_model_v15 = pipette_load_name.convert_pipette_model( - cast(PipetteModel, "p10_single_v1.5") - ) - converted_model_v1 = pipette_load_name.convert_pipette_model( - cast(PipetteModel, "p1000_single_v1") - ) - - mock_get_attached_instr.return_value[types.Mount.LEFT] = { - "config": load_pipette_data.load_definition( - converted_model_v15.pipette_type, - converted_model_v15.pipette_channels, - converted_model_v15.pipette_version, +@pytest.mark.parametrize( + ("protocol_file", "expected_entries"), + [ + ( + "testosaur_v2.py", + [ + "Picking up tip from A1 of Opentrons 96 Tip Rack 1000 µL on 1", + "Aspirating 100.0 uL from A1 of Corning 96 Well Plate 360 µL Flat on 2 at 500.0 uL/sec", + "Dispensing 100.0 uL into B1 of Corning 96 Well Plate 360 µL Flat on 2 at 1000.0 uL/sec", + "Dropping tip into H12 of Opentrons 96 Tip Rack 1000 µL on 1", + ], ), - "id": "testid", - } - mock_get_attached_instr.return_value[types.Mount.RIGHT] = { - "config": load_pipette_data.load_definition( - converted_model_v1.pipette_type, - converted_model_v1.pipette_channels, - converted_model_v1.pipette_version, + ( + # FIXME(2023-10-04): This run log is wrong. It should match the one above. + # https://opentrons.atlassian.net/browse/RSS-368 + "testosaur_v2_14.py", + [ + "Picking up tip from A1 of None", + "Aspirating 100.0 uL from A1 of None at 500.0 uL/sec", + "Dispensing 100.0 uL into B1 of None at 1000.0 uL/sec", + "Dropping tip into H12 of None", + ], ), - "id": "testid2", - } - entries = [] - - def emit_runlog(entry: Any) -> None: - nonlocal entries - entries.append(entry) - - execute.execute(protocol.filelike, protocol.filename, emit_runlog=emit_runlog) - - assert [item["payload"]["text"] for item in entries if item["$"] == "before"] == [ - "Picking up tip from A1 of Opentrons 96 Tip Rack 1000 µL on 1", - "Aspirating 100.0 uL from A1 of Corning 96 Well Plate 360 µL Flat on 2 at 500.0 uL/sec", - "Dispensing 100.0 uL into B1 of Corning 96 Well Plate 360 µL Flat on 2 at 1000.0 uL/sec", - "Dropping tip into H12 of Opentrons 96 Tip Rack 1000 µL on 1", - ] - - -# TODO(mm, 2023-09-26): Merge this with the above test_execute_apiv2_14() function when -# we resolve https://opentrons.atlassian.net/browse/RSS-320 and PAPIv≥2.14 protocols emit -# human-readable run log text. -@pytest.mark.parametrize("protocol_file", ["testosaur_v2_14.py"]) -def test_execute_function_apiv2_14( + ], +) +def test_execute_function_apiv2( protocol: Protocol, protocol_file: str, + expected_entries: List[str], virtual_smoothie_env: None, mock_get_attached_instr: mock.AsyncMock, ) -> None: @@ -147,27 +122,9 @@ def emit_runlog(entry: Any) -> None: execute.execute(protocol.filelike, protocol.filename, emit_runlog=emit_runlog) - # https://opentrons.atlassian.net/browse/RSS-320: - # PAPIv≥2.14 protocols currently emit JSON run log text, not human-readable text. - # Their exact contents can't be tested here because they're too verbose and they have - # unpredictable fields like `createdAt` and `id`. So as an approximation, we just test - # the command types. - command_types = [ - json.loads(item["payload"]["text"])["commandType"] - for item in entries - if item["$"] == "before" - ] - assert command_types == [ - "home", - "home", - "loadLabware", - "loadPipette", - "loadLabware", - "pickUpTip", - "aspirate", - "dispense", - "dropTip", - ] + assert [ + item["payload"]["text"] for item in entries if item["$"] == "before" + ] == expected_entries def test_execute_function_json_v3( From dc4fe4685acf0d2d3095be8dfcaecc2241460a49 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 10 Oct 2023 12:28:25 -0400 Subject: [PATCH 44/79] fix(odd): pretend to be online all the time (#13746) For some reason, when the ODD starts and the robot boots while not connected to any network, some (some!) requests to localhost will fail. It's really strange. One thing that seems to help is pretending to be online by overriding the navigator.onLine browser api to always return true. --------- Co-authored-by: Jamey H --- app-shell-odd/Makefile | 4 ++-- app/src/App/OnDeviceDisplayApp.tsx | 8 +++++++- app/src/App/__mocks__/hacks.ts | 1 + app/src/App/hacks.ts | 16 ++++++++++++++++ scripts/setup-global-mocks.js | 1 + 5 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 app/src/App/__mocks__/hacks.ts create mode 100644 app/src/App/hacks.ts diff --git a/app-shell-odd/Makefile b/app-shell-odd/Makefile index 5dd7ca92736..309beca156d 100644 --- a/app-shell-odd/Makefile +++ b/app-shell-odd/Makefile @@ -48,7 +48,7 @@ setup: .PHONY: clean clean: - shx rm -rf lib dist + shx rm -rf lib dist opentrons-robot-app.tar.gz # artifacts ##################################################################### @@ -75,7 +75,7 @@ push-ot3: dist-ot3 scp $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) -r ./opentrons-robot-app.tar.gz root@$(host): ssh $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) root@$(host) "mount -o remount,rw / && systemctl stop opentrons-robot-app && rm -rf /opt/opentrons-app && mkdir -p /opt/opentrons-app" ssh $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) root@$(host) "tar -xvf opentrons-robot-app.tar.gz -C /opt/opentrons-app/ && mount -o remount,ro / && systemctl start opentrons-robot-app && rm -rf opentrons-robot-app.tar.gz" - rm -rf opentrons-robot-app.tar.gz + # development ##################################################################### diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 6cc63af8986..a8663707b93 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -47,9 +47,15 @@ import { useCurrentRunRoute, useProtocolReceiptToast } from './hooks' import { OnDeviceDisplayAppFallback } from './OnDeviceDisplayAppFallback' +import { hackWindowNavigatorOnLine } from './hacks' + import type { Dispatch } from '../redux/types' import type { RouteProps } from './types' +// forces electron to think we're online which means axios won't elide +// network calls to localhost. see ./hacks.ts for more. +hackWindowNavigatorOnLine() + export const onDeviceDisplayRoutes: RouteProps[] = [ { Component: InitialLoadingScreen, @@ -257,7 +263,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => { // TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals return ( - + {isIdle ? ( diff --git a/app/src/App/__mocks__/hacks.ts b/app/src/App/__mocks__/hacks.ts new file mode 100644 index 00000000000..842209dfcba --- /dev/null +++ b/app/src/App/__mocks__/hacks.ts @@ -0,0 +1 @@ +export const hackWindowNavigatorOnLine = (): void => {} diff --git a/app/src/App/hacks.ts b/app/src/App/hacks.ts new file mode 100644 index 00000000000..696e7baec9a --- /dev/null +++ b/app/src/App/hacks.ts @@ -0,0 +1,16 @@ +// If the system boots while no network connection is available, then some requests to localhost +// hang eternally while connecting (so no request timeouts work either) and things get +// generally weird. Overriding the browser API to pretend to always be "online" fixes this. +// It makes sense; if "onLine" is false, that means that any network call is _guaranteed_ to fail +// so middlewares probably elide them; but we really want it to be true basically always because +// most of what we do is via localhost. +// +// This function is exposed in its own module so it can be mocked in testing +// since jest really doesn't like you doing this. + +export const hackWindowNavigatorOnLine = (): void => { + Object.defineProperty(window.navigator, 'onLine', { + get: () => true, + }) + window.dispatchEvent(new Event('online')) +} diff --git a/scripts/setup-global-mocks.js b/scripts/setup-global-mocks.js index 7f5188f9eb5..79333ac496c 100644 --- a/scripts/setup-global-mocks.js +++ b/scripts/setup-global-mocks.js @@ -16,6 +16,7 @@ jest.mock('../app/src/pages/Labware/helpers/getAllDefs') jest.mock('../app/src/logger') jest.mock('../app/src/App/portal') jest.mock('../app/src/redux/shell/remote') +jest.mock('../app/src/App/hacks') jest.mock('../app-shell/src/config') jest.mock('../app-shell/src/log') jest.mock('../app-shell-odd/src/config') From 27e594a6ebb07b55b87bdc2122de2ce6f1416844 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 10 Oct 2023 15:20:44 -0400 Subject: [PATCH 45/79] feat(api): Replace obsolete `machine` args with `robot_type` (#13737) * Fix bugs in Jupyter override test cases. * Move find_jupyter_labware() to entrypoint_utils. * Remove obsolete `machine` args. Replace `machine` arg of `simulate.get_protocol_api()` with `robot_type`. Remove `machine` arg of `simulate.simulate()`. Remove MachineType type. * Various minor refactors. --- api/src/opentrons/execute.py | 29 +++-- api/src/opentrons/hardware_control/types.py | 2 - api/src/opentrons/simulate.py | 122 +++++++++++++------- api/src/opentrons/util/entrypoint_util.py | 23 +++- api/tests/opentrons/test_execute.py | 45 +++++--- api/tests/opentrons/test_simulate.py | 45 +++++--- 6 files changed, 178 insertions(+), 88 deletions(-) diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index 2f4a7fdc9e9..5a3f30743bb 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -33,8 +33,6 @@ from opentrons.commands import types as command_types -from opentrons.config import IS_ROBOT, JUPYTER_NOTEBOOK_LABWARE_DIR - from opentrons.hardware_control import ( API as OT2API, HardwareControlAPI, @@ -73,6 +71,7 @@ from .util.entrypoint_util import ( FoundLabware, + find_jupyter_labware, labware_from_paths, datafiles_from_paths, copy_file_like, @@ -123,6 +122,9 @@ def get_protocol_api( bundled_labware: Optional[Dict[str, "LabwareDefinitionDict"]] = None, bundled_data: Optional[Dict[str, bytes]] = None, extra_labware: Optional[Dict[str, "LabwareDefinitionDict"]] = None, + # If you add any more arguments here, make sure they're kw-only to make mistakes harder in + # environments without type checking, like Jupyter Notebook. + # * ) -> protocol_api.ProtocolContext: """ Build and return a ``protocol_api.ProtocolContext`` @@ -169,7 +171,8 @@ def get_protocol_api( if extra_labware is None: extra_labware = { - uri: details.definition for uri, details in _get_jupyter_labware().items() + uri: details.definition + for uri, details in (find_jupyter_labware() or {}).items() } robot_type = _get_robot_type() @@ -195,7 +198,7 @@ def get_protocol_api( context = _create_live_context_pe( api_version=checked_version, robot_type=robot_type, - deck_type=guess_deck_type_from_global_config(), + deck_type=deck_type, hardware_api=_THREAD_MANAGED_HW, # type: ignore[arg-type] bundled_data=bundled_data, extra_labware=extra_labware, @@ -362,10 +365,13 @@ def execute( # noqa: C901 contents = protocol_file.read() + # TODO(mm, 2023-10-02): Switch this truthy check to `is not None` + # to match documented behavior. + # See notes in https://github.com/Opentrons/opentrons/pull/13107 if custom_labware_paths: extra_labware = labware_from_paths(custom_labware_paths) else: - extra_labware = _get_jupyter_labware() + extra_labware = find_jupyter_labware() or {} if custom_data_paths: extra_data = datafiles_from_paths(custom_data_paths) @@ -393,6 +399,8 @@ def execute( # noqa: C901 # Guard against trying to run protocols for the wrong robot type. # This matches what robot-server does. + # FIXME: This exposes the internal strings "OT-2 Standard" and "OT-3 Standard". + # https://opentrons.atlassian.net/browse/RSS-370 if protocol.robot_type != _get_robot_type(): raise RuntimeError( f'This robot is of type "{_get_robot_type()}",' @@ -689,17 +697,6 @@ def _get_protocol_engine_config() -> Config: ) -def _get_jupyter_labware() -> Dict[str, FoundLabware]: - """Return labware files in this robot's Jupyter Notebook directory.""" - if IS_ROBOT: - # JUPYTER_NOTEBOOK_LABWARE_DIR should never be None when IS_ROBOT == True. - assert JUPYTER_NOTEBOOK_LABWARE_DIR is not None - if JUPYTER_NOTEBOOK_LABWARE_DIR.is_dir(): - return labware_from_paths([JUPYTER_NOTEBOOK_LABWARE_DIR]) - - return {} - - @contextlib.contextmanager def _adapt_protocol_source( protocol_file: Union[BinaryIO, TextIO], diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index 94e156b9802..fe5e842323e 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -8,8 +8,6 @@ MODULE_LOG = logging.getLogger(__name__) -MachineType = Literal["ot2", "ot3"] - class MotionChecks(enum.Enum): NONE = 0 diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index 0ce49687cfe..bbf3ae73f85 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -10,7 +10,6 @@ import pathlib import queue from typing import ( - cast, Any, Dict, List, @@ -21,6 +20,7 @@ Optional, Union, ) +from typing_extensions import Literal import opentrons from opentrons import should_use_ot3 @@ -29,14 +29,13 @@ ThreadManager, ThreadManagedHardware, ) -from opentrons.hardware_control.types import MachineType from opentrons.hardware_control.simulator_setup import load_simulator from opentrons.protocol_api import MAX_SUPPORTED_VERSION from opentrons.protocols.duration import DurationEstimator from opentrons.protocols.execution import execute from opentrons.legacy_broker import LegacyBroker -from opentrons.config import IS_ROBOT, JUPYTER_NOTEBOOK_LABWARE_DIR +from opentrons.config import IS_ROBOT from opentrons import protocol_api from opentrons.commands import types as command_types @@ -47,8 +46,13 @@ ) from opentrons.protocols.api_support.types import APIVersion from opentrons_shared_data.labware.dev_types import LabwareDefinition +from opentrons_shared_data.robot.dev_types import RobotType -from .util.entrypoint_util import labware_from_paths, datafiles_from_paths +from .util.entrypoint_util import ( + find_jupyter_labware, + labware_from_paths, + datafiles_from_paths, +) # See Jira RCORE-535. @@ -70,6 +74,14 @@ ) +# TODO(mm, 2023-10-05): Deduplicate this with opentrons.protocols.parse(). +_UserSpecifiedRobotType = Literal["OT-2", "Flex"] +"""The user-facing robot type specifier. + +This should match what `opentrons.protocols.parse()` accepts in a protocol's `requirements` dict. +""" + + class AccumulatingHandler(logging.Handler): def __init__( self, @@ -161,9 +173,10 @@ def get_protocol_api( bundled_data: Optional[Dict[str, bytes]] = None, extra_labware: Optional[Dict[str, LabwareDefinition]] = None, hardware_simulator: Optional[ThreadManagedHardware] = None, - # TODO(mm, 2022-12-14): The name and type of this parameter should be unified with - # robotType in a standalone Python protocol's `requirements` dict. Jira RCORE-318. - machine: Optional[MachineType] = None, + # Additional arguments are kw-only to make mistakes harder in environments without + # type checking, like Jupyter Notebook. + *, + robot_type: Optional[_UserSpecifiedRobotType] = None, ) -> protocol_api.ProtocolContext: """ Build and return a ``protocol_api.ProtocolContext`` @@ -198,8 +211,9 @@ def get_protocol_api( it will look for labware in the ``labware`` subdirectory of the Jupyter data directory. :param hardware_simulator: If specified, a hardware simulator instance. - :param machine: Either `"ot2"` or `"ot3"`. If `None`, machine will be - determined from persistent settings. + :param robot_type: The type of robot to simulate: either ``"Flex"`` or ``"OT-2"``. + If you're running this function on a robot, the default is the type of that + robot. Otherwise, the default is ``"OT-2"``, for backwards compatibility. :return: The protocol context. """ if isinstance(version, str): @@ -208,19 +222,27 @@ def get_protocol_api( raise TypeError("version must be either a string or an APIVersion") else: checked_version = version - if ( - extra_labware is None - and IS_ROBOT - and JUPYTER_NOTEBOOK_LABWARE_DIR.is_dir() # type: ignore[union-attr] - ): + + current_robot_type = _get_current_robot_type() + if robot_type is None: + if current_robot_type is None: + parsed_robot_type: RobotType = "OT-2 Standard" + else: + parsed_robot_type = current_robot_type + else: + # TODO(mm, 2023-10-09): This raises a slightly wrong error message, mentioning the camelCase + # `robotType` field in Python files instead of the snake_case `robot_type` argument for this + # function. + parsed_robot_type = parse.robot_type_from_python_identifier(robot_type) + _validate_can_simulate_for_robot_type(parsed_robot_type) + + if extra_labware is None: extra_labware = { uri: details.definition - for uri, details in labware_from_paths( - [str(JUPYTER_NOTEBOOK_LABWARE_DIR)] - ).items() + for uri, details in (find_jupyter_labware() or {}).items() } - checked_hardware = _check_hardware_simulator(hardware_simulator, machine) + checked_hardware = _check_hardware_simulator(hardware_simulator, parsed_robot_type) return _build_protocol_context( version=checked_version, hardware_simulator=checked_hardware, @@ -231,18 +253,16 @@ def get_protocol_api( def _check_hardware_simulator( - hardware_simulator: Optional[ThreadManagedHardware], machine: Optional[MachineType] + hardware_simulator: Optional[ThreadManagedHardware], robot_type: RobotType ) -> ThreadManagedHardware: - # TODO(mm, 2022-12-14): This should fail with a more descriptive error if someone - # runs this on a robot, and that robot doesn't have a matching robot type. - # Jira RCORE-318. if hardware_simulator: return hardware_simulator - elif machine == "ot3" or should_use_ot3(): + elif robot_type == "OT-3 Standard": + # Local import because this isn't available on OT-2s. from opentrons.hardware_control.ot3api import OT3API return ThreadManager(OT3API.build_hardware_simulator) - else: + elif robot_type == "OT-2 Standard": return ThreadManager(OT2API.build_hardware_simulator) @@ -276,6 +296,32 @@ def _build_protocol_context( return context +def _get_current_robot_type() -> Optional[RobotType]: + """Return the type of robot that we're running on, or None if we're not on a robot.""" + if IS_ROBOT: + return "OT-3 Standard" if should_use_ot3() else "OT-2 Standard" + else: + return None + + +def _validate_can_simulate_for_robot_type(robot_type: RobotType) -> None: + """Raise if this device cannot simulate protocols written for the given robot type.""" + current_robot_type = _get_current_robot_type() + if current_robot_type is None: + # When installed locally, this package can simulate protocols for any robot type. + pass + elif robot_type != current_robot_type: + # Match robot server behavior: raise an early error if we're on a robot and the caller + # tries to simulate a protocol written for a different robot type. + + # FIXME: This exposes the internal strings "OT-2 Standard" and "OT-3 Standard". + # https://opentrons.atlassian.net/browse/RSS-370 + raise RuntimeError( + f'This robot is of type "{current_robot_type}",' + f' so it can\'t simulate protocols for robot type "{robot_type}"' + ) + + def bundle_from_sim( protocol: PythonProtocol, context: opentrons.protocol_api.ProtocolContext ) -> BundleContents: @@ -308,10 +354,6 @@ def simulate( # noqa: C901 hardware_simulator_file_path: Optional[str] = None, duration_estimator: Optional[DurationEstimator] = None, log_level: str = "warning", - # TODO(mm, 2022-12-14): Now that protocols declare their target robot types - # intrinsically, the `machine` param should be removed in favor of determining - # it automatically. - machine: Optional[MachineType] = None, ) -> Tuple[List[Mapping[str, Any]], Optional[BundleContents]]: """ Simulate the protocol itself. @@ -372,8 +414,6 @@ def simulate( # noqa: C901 :param log_level: The level of logs to capture in the runlog: ``"debug"``, ``"info"``, ``"warning"``, or ``"error"``. Defaults to ``"warning"``. - :param machine: Either `"ot2"` or `"ot3"`. If `None`, machine will be - determined from persistent settings. :returns: A tuple of a run log for user output, and possibly the required data to write to a bundle to bundle this protocol. The bundle is only emitted if bundling is allowed @@ -384,13 +424,14 @@ def simulate( # noqa: C901 stack_logger.propagate = propagate_logs contents = protocol_file.read() + + # TODO(mm, 2023-10-02): Switch this truthy check to `is not None` + # to match documented behavior. + # See notes in https://github.com/Opentrons/opentrons/pull/13107 if custom_labware_paths: - extra_labware = { - uri: details.definition - for uri, details in labware_from_paths(custom_labware_paths).items() - } + extra_labware = labware_from_paths(custom_labware_paths) else: - extra_labware = {} + extra_labware = find_jupyter_labware() or {} if custom_data_paths: extra_data = datafiles_from_paths(custom_data_paths) @@ -407,7 +448,12 @@ def simulate( # noqa: C901 try: protocol = parse.parse( - contents, file_name, extra_labware=extra_labware, extra_data=extra_data + contents, + file_name, + extra_labware={ + uri: details.definition for uri, details in extra_labware.items() + }, + extra_data=extra_data, ) except parse.JSONSchemaVersionTooNewError as e: if e.attempted_schema_version == 6: @@ -429,7 +475,7 @@ def simulate( # noqa: C901 bundled_data=getattr(protocol, "bundled_data", None), hardware_simulator=hardware_simulator, extra_labware=gpa_extras, - machine=machine, + robot_type="Flex" if protocol.robot_type == "OT-3 Standard" else "OT-2", ) except protocol_api.ProtocolEngineCoreRequiredError as e: raise NotImplementedError(_PYTHON_TOO_NEW_MESSAGE) from e # See Jira RCORE-535. @@ -619,7 +665,6 @@ def get_arguments(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: choices=["runlog", "nothing"], default="runlog", ) - parser.add_argument("-m", "--machine", choices=["ot2", "ot3"]) return parser @@ -666,7 +711,6 @@ def main() -> int: duration_estimator=duration_estimator, hardware_simulator_file_path=getattr(args, "custom_hardware_simulator_file"), log_level=args.log_level, - machine=cast(Optional[MachineType], args.machine), ) if maybe_bundle: diff --git a/api/src/opentrons/util/entrypoint_util.py b/api/src/opentrons/util/entrypoint_util.py index 954d837c2f3..eb2d03bc629 100644 --- a/api/src/opentrons/util/entrypoint_util.py +++ b/api/src/opentrons/util/entrypoint_util.py @@ -6,15 +6,18 @@ from json import JSONDecodeError import pathlib import shutil -from typing import BinaryIO, Dict, Sequence, TextIO, Union, TYPE_CHECKING +from typing import BinaryIO, Dict, Sequence, TextIO, Optional, Union, TYPE_CHECKING from jsonschema import ValidationError # type: ignore +from opentrons.config import IS_ROBOT, JUPYTER_NOTEBOOK_LABWARE_DIR from opentrons.protocol_api import labware from opentrons.calibration_storage import helpers if TYPE_CHECKING: from opentrons_shared_data.labware.dev_types import LabwareDefinition + + log = logging.getLogger(__name__) @@ -64,6 +67,24 @@ def labware_from_paths( return labware_defs +def find_jupyter_labware() -> Optional[Dict[str, FoundLabware]]: + """Return labware files in this robot's Jupyter Notebook directory. + + Returns: + If we're running on an Opentrons robot: + A dict, keyed by labware URI, where each value has the file path and the parsed def. + + Otherwise: None. + """ + if IS_ROBOT: + # JUPYTER_NOTEBOOK_LABWARE_DIR should never be None when IS_ROBOT == True. + assert JUPYTER_NOTEBOOK_LABWARE_DIR is not None + if JUPYTER_NOTEBOOK_LABWARE_DIR.is_dir(): + return labware_from_paths([JUPYTER_NOTEBOOK_LABWARE_DIR]) + + return None + + def datafiles_from_paths(paths: Sequence[Union[str, pathlib.Path]]) -> Dict[str, bytes]: datafiles: Dict[str, bytes] = {} for strpath in paths: diff --git a/api/tests/opentrons/test_execute.py b/api/tests/opentrons/test_execute.py index 9a4ac9fb673..97560d02ef0 100644 --- a/api/tests/opentrons/test_execute.py +++ b/api/tests/opentrons/test_execute.py @@ -21,6 +21,7 @@ from opentrons.hardware_control import Controller, api from opentrons.protocol_api.core.engine import ENGINE_CORE_API_VERSION from opentrons.protocols.api_support.types import APIVersion +from opentrons.util import entrypoint_util if TYPE_CHECKING: from tests.opentrons.conftest import Bundle, Protocol @@ -359,8 +360,12 @@ def test_jupyter( monkeypatch: pytest.MonkeyPatch, ) -> None: """Putting labware in the Jupyter directory should make it available.""" - monkeypatch.setattr(execute, "IS_ROBOT", True) - monkeypatch.setattr(execute, "JUPYTER_NOTEBOOK_LABWARE_DIR", self.LW_DIR) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) + monkeypatch.setattr( + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", self.LW_DIR + ) execute.execute(protocol_file=protocol_filelike, protocol_name=protocol_name) @pytest.mark.xfail( @@ -373,8 +378,12 @@ def test_jupyter_override( monkeypatch: pytest.MonkeyPatch, ) -> None: """Passing any custom_labware_paths should prevent searching the Jupyter directory.""" - monkeypatch.setattr(execute, "IS_ROBOT", True) - monkeypatch.setattr(execute, "JUPYTER_NOTEBOOK_LABWARE_DIR", self.LW_DIR) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) + monkeypatch.setattr( + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", self.LW_DIR + ) with pytest.raises(Exception, match="Labware .+ not found"): execute.execute( protocol_file=protocol_filelike, @@ -389,9 +398,11 @@ def test_jupyter_not_on_filesystem( monkeypatch: pytest.MonkeyPatch, ) -> None: """It should tolerate the Jupyter labware directory not existing on the filesystem.""" - monkeypatch.setattr(execute, "IS_ROBOT", True) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) monkeypatch.setattr( - execute, "JUPYTER_NOTEBOOK_LABWARE_DIR", HERE / "nosuchdirectory" + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", HERE / "nosuchdirectory" ) with pytest.raises(Exception, match="Labware .+ not found"): execute.execute( @@ -437,9 +448,11 @@ def test_jupyter( self, api_version: APIVersion, monkeypatch: pytest.MonkeyPatch ) -> None: """Putting labware in the Jupyter directory should make it available.""" - monkeypatch.setattr(execute, "IS_ROBOT", True) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) monkeypatch.setattr( - execute, + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", get_shared_data_root() / self.LW_FIXTURE_DIR, ) @@ -448,20 +461,19 @@ def test_jupyter( load_name=self.LW_LOAD_NAME, location=1, namespace=self.LW_NAMESPACE ) - @pytest.mark.xfail( - strict=True, raises=pytest.fail.Exception - ) # TODO(mm, 2023-07-14): Fix this bug. def test_jupyter_override( self, api_version: APIVersion, monkeypatch: pytest.MonkeyPatch ) -> None: """Passing any extra_labware should prevent searching the Jupyter directory.""" - monkeypatch.setattr(execute, "IS_ROBOT", True) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) monkeypatch.setattr( - execute, + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", get_shared_data_root() / self.LW_FIXTURE_DIR, ) - context = execute.get_protocol_api(api_version) + context = execute.get_protocol_api(api_version, extra_labware={}) with pytest.raises(Exception, match="Labware .+ not found"): context.load_labware( load_name=self.LW_LOAD_NAME, location=1, namespace=self.LW_NAMESPACE @@ -471,8 +483,11 @@ def test_jupyter_not_on_filesystem( self, api_version: APIVersion, monkeypatch: pytest.MonkeyPatch ) -> None: """It should tolerate the Jupyter labware directory not existing on the filesystem.""" + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) monkeypatch.setattr( - execute, "JUPYTER_NOTEBOOK_LABWARE_DIR", HERE / "nosuchdirectory" + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", HERE / "nosuchdirectory" ) with_nonexistent_jupyter_extra_labware = execute.get_protocol_api(api_version) with pytest.raises(Exception, match="Labware .+ not found"): diff --git a/api/tests/opentrons/test_simulate.py b/api/tests/opentrons/test_simulate.py index 93df57651a9..4336df3b116 100644 --- a/api/tests/opentrons/test_simulate.py +++ b/api/tests/opentrons/test_simulate.py @@ -15,6 +15,7 @@ from opentrons.protocols.types import ApiDeprecationError from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.execution.errors import ExceptionInProtocolError +from opentrons.util import entrypoint_util if TYPE_CHECKING: from tests.opentrons.conftest import Bundle, Protocol @@ -195,8 +196,12 @@ def test_jupyter( monkeypatch: pytest.MonkeyPatch, ) -> None: """Putting labware in the Jupyter directory should make it available.""" - monkeypatch.setattr(simulate, "IS_ROBOT", True) - monkeypatch.setattr(simulate, "JUPYTER_NOTEBOOK_LABWARE_DIR", self.LW_DIR) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) + monkeypatch.setattr( + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", self.LW_DIR + ) simulate.simulate(protocol_file=protocol_filelike, file_name=file_name) @pytest.mark.xfail( @@ -209,8 +214,12 @@ def test_jupyter_override( monkeypatch: pytest.MonkeyPatch, ) -> None: """Passing any custom_labware_paths should prevent searching the Jupyter directory.""" - monkeypatch.setattr(simulate, "IS_ROBOT", True) - monkeypatch.setattr(simulate, "JUPYTER_NOTEBOOK_LABWARE_DIR", self.LW_DIR) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) + monkeypatch.setattr( + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", self.LW_DIR + ) with pytest.raises(Exception, match="Labware .+ not found"): simulate.simulate( protocol_file=protocol_filelike, @@ -225,9 +234,11 @@ def test_jupyter_not_on_filesystem( monkeypatch: pytest.MonkeyPatch, ) -> None: """It should tolerate the Jupyter labware directory not existing on the filesystem.""" - monkeypatch.setattr(simulate, "IS_ROBOT", True) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) monkeypatch.setattr( - simulate, "JUPYTER_NOTEBOOK_LABWARE_DIR", HERE / "nosuchdirectory" + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", HERE / "nosuchdirectory" ) with pytest.raises(Exception, match="Labware .+ not found"): simulate.simulate(protocol_file=protocol_filelike, file_name=file_name) @@ -268,9 +279,11 @@ def test_jupyter( self, api_version: APIVersion, monkeypatch: pytest.MonkeyPatch ) -> None: """Putting labware in the Jupyter directory should make it available.""" - monkeypatch.setattr(simulate, "IS_ROBOT", True) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) monkeypatch.setattr( - simulate, + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", get_shared_data_root() / self.LW_FIXTURE_DIR, ) @@ -279,20 +292,19 @@ def test_jupyter( load_name=self.LW_LOAD_NAME, location=1, namespace=self.LW_NAMESPACE ) - @pytest.mark.xfail( - strict=True, raises=pytest.fail.Exception - ) # TODO(mm, 2023-07-14): Fix this bug. def test_jupyter_override( self, api_version: APIVersion, monkeypatch: pytest.MonkeyPatch ) -> None: """Passing any extra_labware should prevent searching the Jupyter directory.""" - monkeypatch.setattr(simulate, "IS_ROBOT", True) + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) monkeypatch.setattr( - simulate, + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", get_shared_data_root() / self.LW_FIXTURE_DIR, ) - context = simulate.get_protocol_api(api_version) + context = simulate.get_protocol_api(api_version, extra_labware={}) with pytest.raises(Exception, match="Labware .+ not found"): context.load_labware( load_name=self.LW_LOAD_NAME, location=1, namespace=self.LW_NAMESPACE @@ -302,8 +314,11 @@ def test_jupyter_not_on_filesystem( self, api_version: APIVersion, monkeypatch: pytest.MonkeyPatch ) -> None: """It should tolerate the Jupyter labware directory not existing on the filesystem.""" + # TODO(mm, 2023-10-06): This is monkeypatching a dependency of a dependency, + # which is too deep. + monkeypatch.setattr(entrypoint_util, "IS_ROBOT", True) monkeypatch.setattr( - simulate, "JUPYTER_NOTEBOOK_LABWARE_DIR", HERE / "nosuchdirectory" + entrypoint_util, "JUPYTER_NOTEBOOK_LABWARE_DIR", HERE / "nosuchdirectory" ) with_nonexistent_jupyter_extra_labware = simulate.get_protocol_api(api_version) with pytest.raises(Exception, match="Labware .+ not found"): From 3b92b1a3e640327d5a7448757360927f5346281c Mon Sep 17 00:00:00 2001 From: Jamey H Date: Wed, 11 Oct 2023 11:19:57 -0400 Subject: [PATCH 46/79] fix(app): Persist UpdateBuildroot during robot restart (#13740) * feat(app): Wire up nice-modal-react Adds support for Nice-Modal-React. * fix(app): fix closing robot update flows during restart step Relocates UpdateBuildroot rendering to the App component, eliminating certain conditional statements in ancestor components that would abruptly close UpdateBuildroot. --- app/package.json | 1 + app/src/App/index.tsx | 5 +- .../Devices/RobotOverviewOverflowMenu.tsx | 18 +--- .../AdvancedTab/RobotServerVersion.tsx | 11 +-- .../AdvancedTab/SoftwareUpdateModal.tsx | 24 ++--- .../__tests__/RobotServerVersion.test.tsx | 13 ++- .../RobotSettings/RobotSettingsAdvanced.tsx | 14 +-- .../__tests__/UpdateBuildroot.test.tsx | 24 +++-- .../RobotSettings/UpdateBuildroot/index.tsx | 95 ++++++++++--------- .../RobotOverviewOverflowMenu.test.tsx | 12 +-- .../__tests__/UpdateRobotBanner.test.tsx | 9 +- app/src/organisms/UpdateRobotBanner/index.tsx | 20 +--- yarn.lock | 5 + 13 files changed, 107 insertions(+), 144 deletions(-) diff --git a/app/package.json b/app/package.json index d2f8dd09ff1..e67b872ccfe 100644 --- a/app/package.json +++ b/app/package.json @@ -18,6 +18,7 @@ }, "homepage": "https://github.com/Opentrons/opentrons", "dependencies": { + "@ebay/nice-modal-react": "1.2.13", "@fontsource/dejavu-sans": "5.0.3", "@fontsource/public-sans": "5.0.3", "@hot-loader/react-dom": "17.0.1", diff --git a/app/src/App/index.tsx b/app/src/App/index.tsx index 752308ca0ff..671660e0a29 100644 --- a/app/src/App/index.tsx +++ b/app/src/App/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { useSelector } from 'react-redux' import { hot } from 'react-hot-loader/root' +import NiceModal from '@ebay/nice-modal-react' import { Flex, POSITION_FIXED, DIRECTION_ROW } from '@opentrons/components' @@ -29,7 +30,9 @@ export const AppComponent = (): JSX.Element | null => { onDrop={stopEvent} > - {isOnDevice ? : } + + {isOnDevice ? : } + ) : null diff --git a/app/src/organisms/Devices/RobotOverviewOverflowMenu.tsx b/app/src/organisms/Devices/RobotOverviewOverflowMenu.tsx index cd92c726723..36da498324e 100644 --- a/app/src/organisms/Devices/RobotOverviewOverflowMenu.tsx +++ b/app/src/organisms/Devices/RobotOverviewOverflowMenu.tsx @@ -22,7 +22,7 @@ import { Divider } from '../../atoms/structure' import { Tooltip } from '../../atoms/Tooltip' import { ChooseProtocolSlideout } from '../../organisms/ChooseProtocolSlideout' import { DisconnectModal } from '../../organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal' -import { UpdateBuildroot } from '../../organisms/Devices/RobotSettings/UpdateBuildroot' +import { handleUpdateBuildroot } from '../../organisms/Devices/RobotSettings/UpdateBuildroot' import { useCurrentRunId } from '../../organisms/ProtocolUpload/hooks' import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' import { UNREACHABLE, CONNECTABLE, REACHABLE } from '../../redux/discovery' @@ -65,10 +65,6 @@ export const RobotOverviewOverflowMenu = ( dispatch(home(robot.name, ROBOT)) } - const [ - showSoftwareUpdateModal, - setShowSoftwareUpdateModal, - ] = React.useState(false) const [ showChooseProtocolSlideout, setShowChooseProtocolSlideout, @@ -87,10 +83,6 @@ export const RobotOverviewOverflowMenu = ( dispatch(checkShellUpdate()) }) - const handleClickUpdateBuildroot: React.MouseEventHandler = () => { - setShowSoftwareUpdateModal(true) - } - const handleClickRun: React.MouseEventHandler = () => { setShowChooseProtocolSlideout(true) } @@ -105,12 +97,6 @@ export const RobotOverviewOverflowMenu = ( return ( - {showSoftwareUpdateModal ? ( - setShowSoftwareUpdateModal(false)} - /> - ) : null} {showDisconnectModal ? ( setShowDisconnectModal(false)} @@ -138,7 +124,7 @@ export const RobotOverviewOverflowMenu = ( > {isRobotOnWrongVersionOfSoftware && !isRobotUnavailable ? ( handleUpdateBuildroot(robot)} data-testid={`RobotOverviewOverflowMenu_updateSoftware_${String( robot.name )}`} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx index f3806a8ed37..6713ab69c0c 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx @@ -18,7 +18,7 @@ import { getRobotApiVersion } from '../../../../redux/discovery' import { getRobotUpdateDisplayInfo } from '../../../../redux/robot-update' import { UpdateRobotBanner } from '../../../UpdateRobotBanner' import { useIsOT3, useRobot } from '../../hooks' -import { UpdateBuildroot } from '../UpdateBuildroot' +import { handleUpdateBuildroot } from '../UpdateBuildroot' import type { State } from '../../../../redux/types' @@ -35,7 +35,6 @@ export function RobotServerVersion({ const { t } = useTranslation(['device_settings', 'shared']) const robot = useRobot(robotName) const isOT3 = useIsOT3(robotName) - const [showVersionInfoModal, setShowVersionInfoModal] = React.useState(false) const { autoUpdateAction } = useSelector((state: State) => { return getRobotUpdateDisplayInfo(state, robotName) }) @@ -45,12 +44,6 @@ export function RobotServerVersion({ return ( <> - {showVersionInfoModal ? ( - setShowVersionInfoModal(false)} - /> - ) : null} {autoUpdateAction !== 'reinstall' && robot != null ? ( @@ -95,7 +88,7 @@ export function RobotServerVersion({ {t('up_to_date')} setShowVersionInfoModal(true)} + onClick={() => handleUpdateBuildroot(robot)} textTransform={TYPOGRAPHY.textTransformCapitalize} > {t('reinstall')} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx index 5f61f63025e..d8d8b0b548b 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx @@ -21,7 +21,7 @@ import { LegacyModal } from '../../../../molecules/LegacyModal' import { CONNECTABLE, REACHABLE } from '../../../../redux/discovery' import { Divider } from '../../../../atoms/structure' import { useRobot } from '../../hooks' -import { UpdateBuildroot } from '../UpdateBuildroot' +import { handleUpdateBuildroot } from '../UpdateBuildroot' const TECHNICAL_CHANGE_LOG_URL = 'https://github.com/Opentrons/opentrons/blob/edge/CHANGELOG.md' @@ -50,22 +50,9 @@ export function SoftwareUpdateModal({ const [showUpdateModal, setShowUpdateModal] = React.useState(false) const robot = useRobot(robotName) - const handleCloseModal = (): void => { - setShowUpdateModal(false) - closeModal() - } - - const handleLaunchUpdateModal: React.MouseEventHandler = e => { - e.preventDefault() - e.stopPropagation() - setShowUpdateModal(true) - } - if (robot?.status !== CONNECTABLE && robot?.status !== REACHABLE) return null - return showUpdateModal ? ( - - ) : ( + return !showUpdateModal ? ( {t('requires_restarting_the_robot')} @@ -119,7 +106,10 @@ export function SoftwareUpdateModal({ {t('remind_me_later')} { + setShowUpdateModal(true) + handleUpdateBuildroot(robot) + }} disabled={currentRunId != null} > {t('update_robot_now')} @@ -127,5 +117,5 @@ export function SoftwareUpdateModal({ - ) + ) : null } diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/RobotServerVersion.test.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/RobotServerVersion.test.tsx index 63b15534638..c9e6fbed7af 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/RobotServerVersion.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/RobotServerVersion.test.tsx @@ -7,7 +7,7 @@ import { getRobotApiVersion } from '../../../../../redux/discovery' import { getRobotUpdateDisplayInfo } from '../../../../../redux/robot-update' import { mockConnectableRobot } from '../../../../../redux/discovery/__fixtures__' import { useRobot } from '../../../hooks' -import { UpdateBuildroot } from '../../UpdateBuildroot' +import { handleUpdateBuildroot } from '../../UpdateBuildroot' import { RobotServerVersion } from '../RobotServerVersion' jest.mock('../../../hooks') @@ -24,8 +24,8 @@ const mockGetBuildrootUpdateDisplayInfo = getRobotUpdateDisplayInfo as jest.Mock const mockUseRobot = useRobot as jest.MockedFunction -const mockUpdateBuildroot = UpdateBuildroot as jest.MockedFunction< - typeof UpdateBuildroot +const mockUpdateBuildroot = handleUpdateBuildroot as jest.MockedFunction< + typeof handleUpdateBuildroot > const MOCK_ROBOT_VERSION = '7.7.7' @@ -40,7 +40,6 @@ const render = () => { describe('RobotSettings RobotServerVersion', () => { beforeEach(() => { - mockUpdateBuildroot.mockReturnValue(
mock update buildroot
) mockUseRobot.mockReturnValue(mockConnectableRobot) mockGetBuildrootUpdateDisplayInfo.mockReturnValue({ autoUpdateAction: 'reinstall', @@ -66,7 +65,7 @@ describe('RobotSettings RobotServerVersion', () => { getByText('up to date') const reinstall = getByRole('button', { name: 'reinstall' }) fireEvent.click(reinstall) - getByText('mock update buildroot') + expect(mockUpdateBuildroot).toHaveBeenCalled() }) it('should render the warning message if the robot server version needs to upgrade', () => { @@ -81,7 +80,7 @@ describe('RobotSettings RobotServerVersion', () => { ) const btn = getByText('View update') fireEvent.click(btn) - getByText('mock update buildroot') + expect(mockUpdateBuildroot).toHaveBeenCalled() }) it('should render the warning message if the robot server version needs to downgrade', () => { @@ -96,7 +95,7 @@ describe('RobotSettings RobotServerVersion', () => { ) const btn = getByText('View update') fireEvent.click(btn) - getByText('mock update buildroot') + expect(mockUpdateBuildroot).toHaveBeenCalled() }) it('the link should have the correct href', () => { diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx index 06f6b3dc0ec..874456994af 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -38,7 +38,7 @@ import { import { RenameRobotSlideout } from './AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout' import { DeviceResetSlideout } from './AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout' import { DeviceResetModal } from './AdvancedTab/AdvancedTabSlideouts/DeviceResetModal' -import { UpdateBuildroot } from './UpdateBuildroot' +import { handleUpdateBuildroot } from './UpdateBuildroot' import { UNREACHABLE } from '../../../redux/discovery' import { Portal } from '../../../App/portal' @@ -70,10 +70,6 @@ export function RobotSettingsAdvanced({ showDeviceResetModal, setShowDeviceResetModal, ] = React.useState(false) - const [ - showSoftwareUpdateModal, - setShowSoftwareUpdateModal, - ] = React.useState(false) const isRobotBusy = useIsRobotBusy({ poll: true }) @@ -124,12 +120,6 @@ export function RobotSettingsAdvanced({ return ( <> - {showSoftwareUpdateModal ? ( - setShowSoftwareUpdateModal(false)} - /> - ) : null} {showRenameRobotSlideout && ( setShowSoftwareUpdateModal(true)} + onUpdateStart={() => handleUpdateBuildroot(robot)} /> diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateBuildroot.test.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateBuildroot.test.tsx index ed18668c9cf..b7e3d475409 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateBuildroot.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateBuildroot.test.tsx @@ -1,10 +1,11 @@ import React from 'react' +import NiceModal from '@ebay/nice-modal-react' import { mockConnectableRobot as mockRobot } from '../../../../../redux/discovery/__fixtures__' import * as RobotUpdate from '../../../../../redux/robot-update' import { mountWithStore, WrapperWithStore } from '@opentrons/components' -import { UpdateBuildroot } from '..' +import { handleUpdateBuildroot } from '..' import { ViewUpdateModal } from '../ViewUpdateModal' import { RobotUpdateProgressModal } from '../RobotUpdateProgressModal' @@ -33,12 +34,16 @@ const getRobotSystemType = RobotUpdate.getRobotSystemType as jest.MockedFunction const MOCK_STATE: State = { mockState: true } as any describe('UpdateBuildroot', () => { - const closeModal = jest.fn() const render = (): WrapperWithStore< - React.ComponentProps + React.ComponentProps > => { - return mountWithStore>( - , + return mountWithStore>( + +