From 1d046eabb1f7065cb603b3aae309c495c2fda017 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Thu, 22 Jul 2021 09:44:15 -0700 Subject: [PATCH] feat(api): Create framework for running protocols against emulation (#8149) * feat(api): Add G-Code only text output mode * Put response property with other properties * Move device parsing to G-Code Watcher * Add smoothie protocols to validate against * feat(api) Add reset methods to all emulated hardware * feat(api): Refactor app.py into ServerManager class * feat(api): Create Protocol Runner * feat(api): Remove unused properties * feat(api): Fix linting errors * feat(api): Add support for settings * feat(api): More linting * feat(api): Cleanup * feat(api): Remove file that was used for testing --- .../hardware_control/emulation/app.py | 86 ++++++++----- .../hardware_control/emulation/magdeck.py | 7 +- .../hardware_control/emulation/smoothie.py | 28 +++-- .../hardware_control/emulation/tempdeck.py | 9 +- .../emulation/thermocycler.py | 17 +-- .../hardware_control/g_code_parsing/g_code.py | 9 +- .../g_code_program/g_code_program.py | 23 +--- .../g_code_program/supported_text_modes.py | 25 +++- .../g_code_parsing/g_code_watcher.py | 24 +++- .../g_code_parsing/protocol_runner.py | 118 ++++++++++++++++++ api/tests/opentrons/conftest.py | 4 + api/tests/opentrons/data/__init__.py | 0 .../g_code_validation_protocols/__init__.py | 0 .../smoothie_protocol.py | 18 +++ .../g_code_parsing/data/expected_values.txt | 13 -- .../data/smoothie_g_code_coverage.txt | 24 ---- .../g_code_parsing/test_protocol_runner.py | 38 ++++++ .../hardware_control/integration/conftest.py | 14 ++- 18 files changed, 338 insertions(+), 119 deletions(-) create mode 100644 api/src/opentrons/hardware_control/g_code_parsing/protocol_runner.py create mode 100644 api/tests/opentrons/data/__init__.py create mode 100644 api/tests/opentrons/data/g_code_validation_protocols/__init__.py create mode 100644 api/tests/opentrons/data/g_code_validation_protocols/smoothie_protocol.py delete mode 100644 api/tests/opentrons/hardware_control/g_code_parsing/data/expected_values.txt delete mode 100644 api/tests/opentrons/hardware_control/g_code_parsing/data/smoothie_g_code_coverage.txt create mode 100644 api/tests/opentrons/hardware_control/g_code_parsing/test_protocol_runner.py diff --git a/api/src/opentrons/hardware_control/emulation/app.py b/api/src/opentrons/hardware_control/emulation/app.py index ee065194397..f301cdf998b 100644 --- a/api/src/opentrons/hardware_control/emulation/app.py +++ b/api/src/opentrons/hardware_control/emulation/app.py @@ -1,6 +1,5 @@ import asyncio import logging - from opentrons.hardware_control.emulation.connection_handler import \ ConnectionHandler from opentrons.hardware_control.emulation.magdeck import MagDeckEmulator @@ -9,7 +8,6 @@ from opentrons.hardware_control.emulation.tempdeck import TempDeckEmulator from opentrons.hardware_control.emulation.thermocycler import ThermocyclerEmulator from opentrons.hardware_control.emulation.smoothie import SmoothieEmulator - logger = logging.getLogger(__name__) @@ -19,37 +17,69 @@ MAGDECK_PORT = 9999 -async def run_server(host: str, port: int, handler: ConnectionHandler) -> None: - """Run a server.""" - server = await asyncio.start_server(handler, host, port) +class ServerManager: + """ + Class to start and stop emulated smoothie and modules. + """ + def __init__(self, settings=Settings()) -> None: + host = settings.host + self._mag_emulator = MagDeckEmulator(parser=Parser()) + self._temp_emulator = TempDeckEmulator(parser=Parser()) + self._therm_emulator = ThermocyclerEmulator(parser=Parser()) + self._smoothie_emulator = SmoothieEmulator( + parser=Parser(), settings=settings.smoothie + ) + + self._mag_server = self._create_server( + host=host, + port=MAGDECK_PORT, + handler=ConnectionHandler(self._mag_emulator), + ) + self._temp_server = self._create_server( + host=host, + port=TEMPDECK_PORT, + handler=ConnectionHandler(self._temp_emulator), + ) + self._therm_server = self._create_server( + host=host, + port=THERMOCYCLER_PORT, + handler=ConnectionHandler(self._therm_emulator), + ) + self._smoothie_server = self._create_server( + host=host, + port=SMOOTHIE_PORT, + handler=ConnectionHandler(self._smoothie_emulator), + ) + + async def run(self): + await asyncio.gather( + self._mag_server, + self._temp_server, + self._therm_server, + self._smoothie_server + ) - async with server: - await server.serve_forever() + @staticmethod + async def _create_server(host: str, port: int, handler: ConnectionHandler) -> None: + """Run a server.""" + server = await asyncio.start_server(handler, host, port) + async with server: + await server.serve_forever() -async def run() -> None: - """Run the module emulators.""" - settings = Settings() - host = settings.host + def reset(self): + self._smoothie_emulator.reset() + self._mag_emulator.reset() + self._temp_emulator.reset() + self._therm_emulator.reset() - await asyncio.gather( - run_server(host=host, - port=MAGDECK_PORT, - handler=ConnectionHandler(MagDeckEmulator(parser=Parser()))), - run_server(host=host, - port=TEMPDECK_PORT, - handler=ConnectionHandler(TempDeckEmulator(parser=Parser()))), - run_server(host=host, - port=THERMOCYCLER_PORT, - handler=ConnectionHandler(ThermocyclerEmulator(parser=Parser()))), - run_server(host=host, - port=SMOOTHIE_PORT, - handler=ConnectionHandler( - SmoothieEmulator(parser=Parser(), settings=settings.smoothie)) - ), - ) + def stop(self): + self._smoothie_server.close() + self._temp_server.close() + self._therm_server.close() + self._mag_server.close() if __name__ == "__main__": logging.basicConfig(format='%(asctime)s:%(message)s', level=logging.DEBUG) - asyncio.run(run()) + asyncio.run(ServerManager().run()) diff --git a/api/src/opentrons/hardware_control/emulation/magdeck.py b/api/src/opentrons/hardware_control/emulation/magdeck.py index 669aad61c4a..6a1692c5d85 100644 --- a/api/src/opentrons/hardware_control/emulation/magdeck.py +++ b/api/src/opentrons/hardware_control/emulation/magdeck.py @@ -21,8 +21,7 @@ class MagDeckEmulator(AbstractEmulator): """Magdeck emulator""" def __init__(self, parser: Parser) -> None: - self.height: float = 0 - self.position: float = 0 + self.reset() self._parser = parser def handle(self, line: str) -> Optional[str]: @@ -31,6 +30,10 @@ def handle(self, line: str) -> Optional[str]: joined = ' '.join(r for r in results if r) return None if not joined else joined + def reset(self): + self.height: float = 0 + self.position: float = 0 + def _handle(self, command: Command) -> Optional[str]: """Handle a command.""" logger.info(f"Got command {command}") diff --git a/api/src/opentrons/hardware_control/emulation/smoothie.py b/api/src/opentrons/hardware_control/emulation/smoothie.py index 7c00df1c3cf..3c0e3e34e07 100644 --- a/api/src/opentrons/hardware_control/emulation/smoothie.py +++ b/api/src/opentrons/hardware_control/emulation/smoothie.py @@ -26,7 +26,17 @@ class SmoothieEmulator(AbstractEmulator): INSTRUMENT_AND_MODEL_STRING_LENGTH = 64 def __init__(self, parser: Parser, settings: SmoothieSettings) -> None: - """Constructor""" + self._parser = parser + self._settings = settings + self.reset() + + def handle(self, line: str) -> Optional[str]: + """Handle a line""" + results = (self._handle(c) for c in self._parser.parse(line)) + joined = ' '.join(r for r in results if r) + return None if not joined else joined + + def reset(self): _, fw_version = _find_smoothie_file() self._version_string = \ f"Build version: {fw_version}, Build date: CURRENT, " \ @@ -52,22 +62,21 @@ def __init__(self, parser: Parser, settings: SmoothieSettings) -> None: self._pipette_model = { "L": utils.string_to_hex( - settings.left.model, self.INSTRUMENT_AND_MODEL_STRING_LENGTH + self._settings.left.model, self.INSTRUMENT_AND_MODEL_STRING_LENGTH ), "R": utils.string_to_hex( - settings.right.model, self.INSTRUMENT_AND_MODEL_STRING_LENGTH + self._settings.right.model, self.INSTRUMENT_AND_MODEL_STRING_LENGTH ), } self._pipette_id = { "L": utils.string_to_hex( - settings.left.id, self.INSTRUMENT_AND_MODEL_STRING_LENGTH + self._settings.left.id, self.INSTRUMENT_AND_MODEL_STRING_LENGTH ), "R": utils.string_to_hex( - settings.right.id, self.INSTRUMENT_AND_MODEL_STRING_LENGTH + self._settings.right.id, self.INSTRUMENT_AND_MODEL_STRING_LENGTH ), } - self._parser = parser self._gcode_to_function_mapping = { GCODE.HOMING_STATUS.value: self._get_homing_status, @@ -81,11 +90,8 @@ def __init__(self, parser: Parser, settings: SmoothieSettings) -> None: GCODE.HOME.value: self._home_gantry, } - def handle(self, line: str) -> Optional[str]: - """Handle a line""" - results = (self._handle(c) for c in self._parser.parse(line)) - joined = ' '.join(r for r in results if r) - return None if not joined else joined + def get_current_position(self): + return self._pos def _get_homing_status(self, command: Command) -> str: """Get the current homing status of the emulated gantry""" diff --git a/api/src/opentrons/hardware_control/emulation/tempdeck.py b/api/src/opentrons/hardware_control/emulation/tempdeck.py index 06635c452c2..45cb44b22f9 100644 --- a/api/src/opentrons/hardware_control/emulation/tempdeck.py +++ b/api/src/opentrons/hardware_control/emulation/tempdeck.py @@ -26,9 +26,7 @@ class TempDeckEmulator(AbstractEmulator): """TempDeck emulator""" def __init__(self, parser: Parser) -> None: - self._temperature = Temperature( - per_tick=.25, current=0.0 - ) + self.reset() self._parser = parser def handle(self, line: str) -> Optional[str]: @@ -37,6 +35,11 @@ def handle(self, line: str) -> Optional[str]: joined = ' '.join(r for r in results if r) return None if not joined else joined + def reset(self): + self._temperature = Temperature( + per_tick=.25, current=0.0 + ) + def _handle(self, command: Command) -> Optional[str]: """Handle a command.""" logger.info(f"Got command {command}") diff --git a/api/src/opentrons/hardware_control/emulation/thermocycler.py b/api/src/opentrons/hardware_control/emulation/thermocycler.py index 5525230dd94..1c337605d45 100644 --- a/api/src/opentrons/hardware_control/emulation/thermocycler.py +++ b/api/src/opentrons/hardware_control/emulation/thermocycler.py @@ -25,6 +25,16 @@ class ThermocyclerEmulator(AbstractEmulator): """Thermocycler emulator""" def __init__(self, parser: Parser) -> None: + self.reset() + self._parser = parser + + def handle(self, line: str) -> Optional[str]: + """Handle a line""" + results = (self._handle(c) for c in self._parser.parse(line)) + joined = ' '.join(r for r in results if r) + return None if not joined else joined + + def reset(self): self._lid_temperate = Temperature( per_tick=2, current=util.TEMPERATURE_ROOM ) @@ -34,13 +44,6 @@ def __init__(self, parser: Parser) -> None: self.lid_status = ThermocyclerLidStatus.OPEN self.plate_volume = util.OptionalValue[float]() self.plate_ramp_rate = util.OptionalValue[float]() - self._parser = parser - - def handle(self, line: str) -> Optional[str]: - """Handle a line""" - results = (self._handle(c) for c in self._parser.parse(line)) - joined = ' '.join(r for r in results if r) - return None if not joined else joined def _handle(self, command: Command) -> Optional[str]: # noqa: C901 """ diff --git a/api/src/opentrons/hardware_control/g_code_parsing/g_code.py b/api/src/opentrons/hardware_control/g_code_parsing/g_code.py index 7d488cf5f93..7e44fb03b23 100644 --- a/api/src/opentrons/hardware_control/g_code_parsing/g_code.py +++ b/api/src/opentrons/hardware_control/g_code_parsing/g_code.py @@ -193,6 +193,11 @@ def g_code_line(self) -> str: """ return f'{self.g_code} {self.g_code_body}'.strip() + @property + def response(self): + """Unparsed G-Code Response""" + return self._response + def get_gcode_function(self) -> str: """ Returns the function that the G-Code performs. @@ -256,7 +261,3 @@ def get_explanation(self) -> Explanation: self.g_code_args, self.response ) - - @property - def response(self): - return self._response diff --git a/api/src/opentrons/hardware_control/g_code_parsing/g_code_program/g_code_program.py b/api/src/opentrons/hardware_control/g_code_parsing/g_code_program/g_code_program.py index be78d183624..8c56f9aa5c1 100644 --- a/api/src/opentrons/hardware_control/g_code_parsing/g_code_program/g_code_program.py +++ b/api/src/opentrons/hardware_control/g_code_parsing/g_code_program/g_code_program.py @@ -1,8 +1,6 @@ from __future__ import annotations import os import json -from opentrons.hardware_control.emulation.app import \ - TEMPDECK_PORT, THERMOCYCLER_PORT, SMOOTHIE_PORT, MAGDECK_PORT from typing import List, Union from opentrons.hardware_control.g_code_parsing.g_code_watcher import GCodeWatcher from opentrons.hardware_control.g_code_parsing.g_code import GCode @@ -14,14 +12,6 @@ class GCodeProgram: Class for parsing various G-Code files and programs into a list of GCode objects """ - - DEVICE_LOOKUP_BY_PORT = { - SMOOTHIE_PORT: 'smoothie', - TEMPDECK_PORT: 'tempdeck', - THERMOCYCLER_PORT: 'thermocycler', - MAGDECK_PORT: 'magdeck', - } - @classmethod def from_g_code_watcher(cls, watcher: GCodeWatcher) -> GCodeProgram: """ @@ -32,11 +22,10 @@ def from_g_code_watcher(cls, watcher: GCodeWatcher) -> GCodeProgram: """ g_codes = [] for watcher_data in watcher.get_command_list(): - device = cls._parse_device(watcher_data.serial_connection) g_codes.extend( GCode.from_raw_code( watcher_data.raw_g_code, - device, + watcher_data.device, watcher_data.response ) ) @@ -45,16 +34,6 @@ def from_g_code_watcher(cls, watcher: GCodeWatcher) -> GCodeProgram: def __init__(self, g_codes: List[GCode]): self._g_codes = g_codes - @classmethod - def _parse_device(cls, serial_connection): - """ - Based on port specified in connection URL, parse out what the name - of the device is - """ - serial_port = serial_connection.port - device_port = serial_port[serial_port.rfind(':') + 1:] - return cls.DEVICE_LOOKUP_BY_PORT[int(device_port)] - def add_g_code(self, g_code: GCode) -> None: """Add singular G-Code to the end of the program""" self._g_codes.append(g_code) diff --git a/api/src/opentrons/hardware_control/g_code_parsing/g_code_program/supported_text_modes.py b/api/src/opentrons/hardware_control/g_code_parsing/g_code_program/supported_text_modes.py index 601f8ddc394..ad52b87a140 100644 --- a/api/src/opentrons/hardware_control/g_code_parsing/g_code_program/supported_text_modes.py +++ b/api/src/opentrons/hardware_control/g_code_parsing/g_code_program/supported_text_modes.py @@ -85,6 +85,23 @@ def concise_builder(code: GCode): return MULTIPLE_SPACE_REGEX.sub(' ', message).strip() +def g_code_only_builder(code: GCode): + """ + Function to build string that contains only the raw G-Code input and output + + -> + + Example: + + G28.2 X -> Homing the following axes: X + + :param code: G-Code object to parse into a string + :return: Textual description + """ + message = f'{code.g_code_line} -> {code.response}' + return MULTIPLE_SPACE_REGEX.sub(' ', message).strip() + + class SupportedTextModes(Enum): """ Class representing the different text modes that G-Codes can be parsed into @@ -101,6 +118,11 @@ class SupportedTextModes(Enum): """ DEFAULT = 'Default' CONCISE = 'Concise' + G_CODE = 'G-Code' + + @classmethod + def get_valid_modes(cls): + return [cls.CONCISE.value, cls.DEFAULT.value, cls.G_CODE.value] @classmethod def get_text_mode(cls, key: str): @@ -108,7 +130,8 @@ def get_text_mode(cls, key: str): # when using the __members__ attribute _internal_mapping = { cls.DEFAULT.value: TextMode(cls.DEFAULT.value, default_builder), - cls.CONCISE.value: TextMode(cls.CONCISE.value, concise_builder) + cls.CONCISE.value: TextMode(cls.CONCISE.value, concise_builder), + cls.G_CODE.value: TextMode(cls.G_CODE.value, g_code_only_builder) } members = [member.value for member in list(cls.__members__.values())] if key not in members: diff --git a/api/src/opentrons/hardware_control/g_code_parsing/g_code_watcher.py b/api/src/opentrons/hardware_control/g_code_parsing/g_code_watcher.py index da74da08351..b51c2e0919a 100644 --- a/api/src/opentrons/hardware_control/g_code_parsing/g_code_watcher.py +++ b/api/src/opentrons/hardware_control/g_code_parsing/g_code_watcher.py @@ -2,12 +2,14 @@ from serial import Serial # type: ignore from opentrons.drivers import serial_communication from dataclasses import dataclass +from opentrons.hardware_control.emulation.app import \ + TEMPDECK_PORT, THERMOCYCLER_PORT, SMOOTHIE_PORT, MAGDECK_PORT @dataclass class WatcherData: raw_g_code: str - serial_connection: Serial + device: str response: str @@ -16,12 +18,30 @@ class GCodeWatcher: Watches commands sent to serial_communication.write_and_return extracts the parameters passed and stores them """ + + DEVICE_LOOKUP_BY_PORT = { + SMOOTHIE_PORT: 'smoothie', + TEMPDECK_PORT: 'tempdeck', + THERMOCYCLER_PORT: 'thermocycler', + MAGDECK_PORT: 'magdeck', + } + def __init__(self) -> None: self._command_list: List[WatcherData] = [] self._old_write_return = serial_communication.write_and_return serial_communication.write_and_return = self._pull_info + @classmethod + def _parse_device(cls, serial_connection: Serial): + """ + Based on port specified in connection URL, parse out what the name + of the device is + """ + serial_port = serial_connection.port + device_port = serial_port[serial_port.rfind(':') + 1:] + return cls.DEVICE_LOOKUP_BY_PORT[int(device_port)] + def _pull_info(self, *args, **kwargs): """ Side-effect function that gathers arguments passed to @@ -36,7 +56,7 @@ def _pull_info(self, *args, **kwargs): self._command_list.append( WatcherData( raw_g_code=args[0], - serial_connection=args[2], + device=self._parse_device(args[2]), response=response ) ) diff --git a/api/src/opentrons/hardware_control/g_code_parsing/protocol_runner.py b/api/src/opentrons/hardware_control/g_code_parsing/protocol_runner.py new file mode 100644 index 00000000000..e02d32ad6d1 --- /dev/null +++ b/api/src/opentrons/hardware_control/g_code_parsing/protocol_runner.py @@ -0,0 +1,118 @@ +import os +import sys +import threading +import asyncio +from typing import Generator +from collections import namedtuple + +from opentrons.hardware_control.emulation.settings import SmoothieSettings +from opentrons.protocols.parse import parse +from opentrons.protocols.execution import execute +from contextlib import contextmanager +from opentrons.protocol_api import ProtocolContext +from opentrons.config.robot_configs import build_config +from opentrons.hardware_control.emulation.app import ServerManager +from opentrons.hardware_control import API, ThreadManager +from opentrons.hardware_control.emulation.app import \ + MAGDECK_PORT,\ + TEMPDECK_PORT,\ + THERMOCYCLER_PORT,\ + SMOOTHIE_PORT +from opentrons.hardware_control.g_code_parsing.g_code_program.g_code_program import \ + GCodeProgram +from opentrons.hardware_control.g_code_parsing.g_code_watcher import GCodeWatcher +from opentrons.protocols.context.protocol_api.protocol_context import \ + ProtocolContextImplementation + + +Protocol = namedtuple( + 'Protocol', + ['text', 'filename', 'filelike']) + + +class ProtocolRunner: + """ + Class for running a Protocol against the emulator. + See src/opentrons/hardware_control/emulation/settings.py for example explanation + of Smoothie configs + + Workflow is as follows: + 1. Instantiate Protocol Runner + 2. Call run_protocol method + 3. Gather parsed data from returned GCodeProgram + """ + URI_TEMPLATE = "socket://127.0.0.1:%s" + + def __init__(self, smoothie_config: SmoothieSettings) -> None: + self._config = smoothie_config + self._set_env_vars() + + @staticmethod + def _get_loop() -> asyncio.AbstractEventLoop: + """Create an event loop""" + if sys.platform == 'win32': + _loop = asyncio.ProactorEventLoop() + else: + _loop = asyncio.new_event_loop() + asyncio.set_event_loop(_loop) + return asyncio.get_event_loop() + + @staticmethod + def _set_env_vars() -> None: + """Set URLs of where to find modules and config for smoothie""" + os.environ['OT_MAGNETIC_EMULATOR_URI'] = \ + ProtocolRunner.URI_TEMPLATE % MAGDECK_PORT + os.environ['OT_THERMOCYCLER_EMULATOR_URI'] = \ + ProtocolRunner.URI_TEMPLATE % THERMOCYCLER_PORT + os.environ['OT_TEMPERATURE_EMULATOR_URI'] = \ + ProtocolRunner.URI_TEMPLATE % TEMPDECK_PORT + + @staticmethod + def _start_emulation_app(server_manager: ServerManager) -> None: + """Start emulated OT-2""" + def runit(): + asyncio.run(server_manager.run()) + t = threading.Thread(target=runit) + t.daemon = True + t.start() + + @staticmethod + def _emulate_hardware() -> ThreadManager: + """Created emulated smoothie""" + conf = build_config({}) + emulator = ThreadManager( + API.build_hardware_controller, + conf, + ProtocolRunner.URI_TEMPLATE % SMOOTHIE_PORT + ) + return emulator + + @staticmethod + def _get_protocol(file_path: str) -> Protocol: + with open(file_path) as file: + text = ''.join(list(file)) + file.seek(0) + + return Protocol(text=text, filename=file_path, filelike=file) + + @contextmanager + def run_protocol(self, file_name: str) -> Generator: + """ + Runs passed protocol file and collects all G-Code I/O from it. + Will cleanup emulation after execution + :param file_name: Path to file + :return: GCodeProgram with all the parsed data + """ + server_manager = ServerManager(self._config) + self._start_emulation_app(server_manager) + emulated_hardware = self._emulate_hardware() + protocol = self._get_protocol(file_name) + context = ProtocolContext( + implementation=ProtocolContextImplementation(hardware=emulated_hardware), + loop=self._get_loop() + ) + parsed_protocol = parse(protocol.text, protocol.filename) + watcher = GCodeWatcher() + execute.run_protocol(parsed_protocol, context=context) + yield GCodeProgram.from_g_code_watcher(watcher) + server_manager.stop() diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index fa1e0d2b3db..4e476654daa 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -239,6 +239,10 @@ async def ctx(loop, hardware) -> ProtocolContext: ) +def data_dir() -> str: + return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') + + def build_v2_model(h, lw_name, loop): ctx = ProtocolContext( implementation=ProtocolContextImplementation(hardware=h), diff --git a/api/tests/opentrons/data/__init__.py b/api/tests/opentrons/data/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/tests/opentrons/data/g_code_validation_protocols/__init__.py b/api/tests/opentrons/data/g_code_validation_protocols/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/tests/opentrons/data/g_code_validation_protocols/smoothie_protocol.py b/api/tests/opentrons/data/g_code_validation_protocols/smoothie_protocol.py new file mode 100644 index 00000000000..8a9e7bc93c7 --- /dev/null +++ b/api/tests/opentrons/data/g_code_validation_protocols/smoothie_protocol.py @@ -0,0 +1,18 @@ +from opentrons import types + +metadata = { + 'protocolName': 'Smoothie Testing', + 'author': 'Derek Maggio', + 'apiLevel': '2.0', +} + + +def run(ctx): + ctx.home() + tr = ctx.load_labware('opentrons_96_tiprack_300ul', 1) + right = ctx.load_instrument('p20_single_gen2', types.Mount.RIGHT, [tr]) + lw = ctx.load_labware('corning_96_wellplate_360ul_flat', 2) + right.pick_up_tip() + right.aspirate(10, lw.wells()[0].bottom()) + right.dispense(10, lw.wells()[1].bottom()) + right.drop_tip(tr.wells()[-1].top()) diff --git a/api/tests/opentrons/hardware_control/g_code_parsing/data/expected_values.txt b/api/tests/opentrons/hardware_control/g_code_parsing/data/expected_values.txt deleted file mode 100644 index db8f280ad5d..00000000000 --- a/api/tests/opentrons/hardware_control/g_code_parsing/data/expected_values.txt +++ /dev/null @@ -1,13 +0,0 @@ -HOME -HOME -HOME -HOME -HOME -HOME -HOME -MOVE -MOVE -MOVE -SET_SPEED -WAIT -SET_CURRENT \ No newline at end of file diff --git a/api/tests/opentrons/hardware_control/g_code_parsing/data/smoothie_g_code_coverage.txt b/api/tests/opentrons/hardware_control/g_code_parsing/data/smoothie_g_code_coverage.txt deleted file mode 100644 index 4d1bf157124..00000000000 --- a/api/tests/opentrons/hardware_control/g_code_parsing/data/smoothie_g_code_coverage.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Home -1625066248.782470 | smoothie | G28.2 ABCXYZ -1625066248.786640 | smoothie | G28.2 X -1625066248.789110 | smoothie | G28.2 Y -1625066248.794110 | smoothie | G28.2 Z -1625066248.795227 | smoothie | G28.2 A -1625066248.795527 | smoothie | G28.2 B -1625066248.802971 | smoothie | G28.2 C - -# Move -1625066248.881811 | smoothie | G0 X113.38 Y11.24 -1625066248.879905 | smoothie | G0 A132.6 -1625066248.875387 | smoothie | G0 C-8.5 - -# Set Speed -1625066248.874877 | smoothie | G0 F601 - -# Wait -1625066248.793615 | smoothie | M400 - -# Set Current -1625066248.804175 | smoothie | M907 A0.1 B0.3 C0.05 X0.3 Y0.3 Z0.1 - -# TODO: Define coverage for the rest of the G-Codes in Smoothie diff --git a/api/tests/opentrons/hardware_control/g_code_parsing/test_protocol_runner.py b/api/tests/opentrons/hardware_control/g_code_parsing/test_protocol_runner.py new file mode 100644 index 00000000000..a2023e9e38d --- /dev/null +++ b/api/tests/opentrons/hardware_control/g_code_parsing/test_protocol_runner.py @@ -0,0 +1,38 @@ +import pytest +import os +from tests.opentrons.conftest import data_dir +from opentrons.hardware_control.g_code_parsing.protocol_runner import ProtocolRunner +from opentrons.hardware_control.g_code_parsing.g_code_program.supported_text_modes \ + import SupportedTextModes +from opentrons.hardware_control.emulation.settings import \ + Settings, SmoothieSettings, PipetteSettings + +CONFIG = Settings( + host='0.0.0.0', + smoothie=SmoothieSettings( + left=PipetteSettings(model='p20_single_v2.0', id='P20SV202020070101'), + right=PipetteSettings(model='p20_single_v2.0', id='P20SV202020070101') + ) +) + +PROTOCOL_PATH = os.path.join( + data_dir(), 'g_code_validation_protocols', 'smoothie_protocol.py' +) + + +@pytest.fixture +def protocol_runner() -> ProtocolRunner: + return ProtocolRunner(CONFIG) + + +async def test_watcher_command_list_is_cleared(protocol_runner: ProtocolRunner): + """ + If everything is cleaning up correctly then 2 runs of the same protocol + should return the same exact G-Code + """ + with protocol_runner.run_protocol(PROTOCOL_PATH) as run_1: + run_1_desc = run_1.get_text_explanation(SupportedTextModes.G_CODE) + with protocol_runner.run_protocol(PROTOCOL_PATH) as run_2: + run_2_desc = run_2.get_text_explanation(SupportedTextModes.G_CODE) + + assert run_1_desc == run_2_desc diff --git a/api/tests/opentrons/hardware_control/integration/conftest.py b/api/tests/opentrons/hardware_control/integration/conftest.py index 5e7a74f82a7..6811a2193ad 100644 --- a/api/tests/opentrons/hardware_control/integration/conftest.py +++ b/api/tests/opentrons/hardware_control/integration/conftest.py @@ -1,14 +1,24 @@ import pytest import threading import asyncio -from opentrons.hardware_control.emulation.app import run +from opentrons.hardware_control.emulation.app import ServerManager +from opentrons.hardware_control.emulation.settings import Settings, SmoothieSettings, \ + PipetteSettings + +CONFIG = Settings( + host='0.0.0.0', + smoothie=SmoothieSettings( + left=PipetteSettings(model='p20_multi_v2.0', id='P3HMV202020041605'), + right=PipetteSettings(model='p20_single_v2.0', id='P20SV202020070101') + ) +) @pytest.fixture(scope="session") def emulation_app(): """Run the emulators""" def runit(): - asyncio.run(run()) + asyncio.run(ServerManager().run()) # TODO 20210219 # The emulators must be run in a separate thread because our serial # drivers block the main thread. Remove this thread when that is no