Skip to content

Commit

Permalink
Merge branch 'edge' into chore_update-vite-to-v5-4-11
Browse files Browse the repository at this point in the history
  • Loading branch information
koji committed Dec 11, 2024
2 parents ece9a6d + 6149597 commit bdc5788
Show file tree
Hide file tree
Showing 28 changed files with 744 additions and 88 deletions.
2 changes: 2 additions & 0 deletions api/src/opentrons/drivers/asyncio/communication/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
NoResponse,
AlarmResponse,
ErrorResponse,
UnhandledGcode,
)
from .async_serial import AsyncSerial

Expand All @@ -15,4 +16,5 @@
"NoResponse",
"AlarmResponse",
"ErrorResponse",
"UnhandledGcode",
]
19 changes: 16 additions & 3 deletions api/src/opentrons/drivers/asyncio/communication/errors.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
"""Errors raised by serial connection."""


from enum import Enum


class ErrorCodes(Enum):
UNHANDLED_GCODE = "ERR003"


class SerialException(Exception):
"""Base serial exception"""

def __init__(self, port: str, description: str):
def __init__(self, port: str, description: str) -> None:
super().__init__(f"{port}: {description}")
self.port = port
self.description = description


class NoResponse(SerialException):
def __init__(self, port: str, command: str):
def __init__(self, port: str, command: str) -> None:
super().__init__(port=port, description=f"No response to '{command}'")
self.command = command


class FailedCommand(SerialException):
def __init__(self, port: str, response: str):
def __init__(self, port: str, response: str) -> None:
super().__init__(
port=port, description=f"'Received error response '{response}'"
)
Expand All @@ -30,3 +37,9 @@ class AlarmResponse(FailedCommand):

class ErrorResponse(FailedCommand):
pass


class UnhandledGcode(ErrorResponse):
def __init__(self, port: str, response: str, command: str) -> None:
self.command = command
super().__init__(port, response)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from opentrons.drivers.command_builder import CommandBuilder

from .errors import NoResponse, AlarmResponse, ErrorResponse
from .errors import NoResponse, AlarmResponse, ErrorResponse, UnhandledGcode, ErrorCodes
from .async_serial import AsyncSerial

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -199,7 +199,7 @@ async def _send_data(self, data: str, retries: int = 0) -> str:
str_response = self.process_raw_response(
command=data, response=response.decode()
)
self.raise_on_error(response=str_response)
self.raise_on_error(response=str_response, request=data)
return str_response

log.info(f"{self.name}: retry number {retry}/{retries}")
Expand Down Expand Up @@ -232,11 +232,12 @@ def name(self) -> str:
def send_data_lock(self) -> asyncio.Lock:
return self._send_data_lock

def raise_on_error(self, response: str) -> None:
def raise_on_error(self, response: str, request: str) -> None:
"""
Raise an error if the response contains an error
Args:
gcode: the requesting gocde
response: response
Returns: None
Expand All @@ -248,8 +249,13 @@ def raise_on_error(self, response: str) -> None:
if self._alarm_keyword in lower:
raise AlarmResponse(port=self._port, response=response)

if self._error_keyword in lower:
raise ErrorResponse(port=self._port, response=response)
if self._error_keyword.lower() in lower:
if ErrorCodes.UNHANDLED_GCODE.value.lower() in lower:
raise UnhandledGcode(
port=self._port, response=response, command=request
)
else:
raise ErrorResponse(port=self._port, response=response)

async def on_retry(self) -> None:
"""
Expand Down Expand Up @@ -454,15 +460,15 @@ async def _send_data(self, data: str, retries: int = 0) -> str:
str_response = self.process_raw_response(
command=data, response=ackless_response.decode()
)
self.raise_on_error(response=str_response)
self.raise_on_error(response=str_response, request=data)

if self._ack in response[-1]:
# Remove ack from response
ackless_response = response[-1].replace(self._ack, b"")
str_response = self.process_raw_response(
command=data, response=ackless_response.decode()
)
self.raise_on_error(response=str_response)
self.raise_on_error(response=str_response, request=data)
return str_response

log.info(f"{self._name}: retry number {retry}/{retries}")
Expand Down
14 changes: 10 additions & 4 deletions api/src/opentrons/drivers/heater_shaker/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from typing import Optional, Dict
from opentrons.drivers import utils
from opentrons.drivers.command_builder import CommandBuilder
from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection
from opentrons.drivers.asyncio.communication import (
AsyncResponseSerialConnection,
UnhandledGcode,
)
from opentrons.drivers.heater_shaker.abstract import AbstractHeaterShakerDriver
from opentrons.drivers.types import Temperature, RPM, HeaterShakerLabwareLatchStatus

Expand Down Expand Up @@ -177,9 +180,12 @@ async def get_device_info(self) -> Dict[str, str]:
reset_reason = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode(
gcode=GCODE.GET_RESET_REASON
)
await self._connection.send_command(
command=reset_reason, retries=DEFAULT_COMMAND_RETRIES
)
try:
await self._connection.send_command(
command=reset_reason, retries=DEFAULT_COMMAND_RETRIES
)
except UnhandledGcode:
pass

return utils.parse_hs_device_information(device_info_string=response)

Expand Down
7 changes: 5 additions & 2 deletions api/src/opentrons/drivers/temp_deck/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from opentrons.drivers import utils
from opentrons.drivers.types import Temperature
from opentrons.drivers.command_builder import CommandBuilder
from opentrons.drivers.asyncio.communication import SerialConnection
from opentrons.drivers.asyncio.communication import SerialConnection, UnhandledGcode
from opentrons.drivers.temp_deck.abstract import AbstractTempDeckDriver

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -163,7 +163,10 @@ async def get_device_info(self) -> Dict[str, str]:
reset_reason = CommandBuilder(
terminator=TEMP_DECK_COMMAND_TERMINATOR
).add_gcode(gcode=GCODE.GET_RESET_REASON)
await self._send_command(command=reset_reason)
try:
await self._send_command(command=reset_reason)
except UnhandledGcode:
pass

return utils.parse_device_information(device_info_string=response)

Expand Down
19 changes: 8 additions & 11 deletions api/src/opentrons/drivers/thermocycler/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
SerialConnection,
AsyncResponseSerialConnection,
AsyncSerial,
UnhandledGcode,
)
from opentrons.drivers.thermocycler.abstract import AbstractThermocyclerDriver
from opentrons.drivers.types import Temperature, PlateTemperature, ThermocyclerLidStatus
Expand Down Expand Up @@ -95,7 +96,7 @@ async def create(
name=port,
ack=TC_GEN2_SERIAL_ACK,
retry_wait_time_seconds=0.1,
error_keyword="error",
error_keyword="err",
alarm_keyword="alarm",
)

Expand Down Expand Up @@ -300,13 +301,6 @@ async def get_device_info(self) -> Dict[str, str]:
command=device_info, retries=DEFAULT_COMMAND_RETRIES
)

reset_reason = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode(
gcode=GCODE.GET_RESET_REASON
)
await self._connection.send_command(
command=reset_reason, retries=DEFAULT_COMMAND_RETRIES
)

return utils.parse_device_information(device_info_string=response)

async def enter_programming_mode(self) -> None:
Expand Down Expand Up @@ -366,9 +360,12 @@ async def get_device_info(self) -> Dict[str, str]:
reset_reason = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode(
gcode=GCODE.GET_RESET_REASON
)
await self._connection.send_command(
command=reset_reason, retries=DEFAULT_COMMAND_RETRIES
)
try:
await self._connection.send_command(
command=reset_reason, retries=DEFAULT_COMMAND_RETRIES
)
except UnhandledGcode:
pass

return utils.parse_hs_device_information(device_info_string=response)

Expand Down
10 changes: 10 additions & 0 deletions api/src/opentrons/protocols/api_support/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,13 @@ def _check_version_wrapper(*args: Any, **kwargs: Any) -> Any:
return cast(FuncT, _check_version_wrapper)

return _set_version


class ModifiedList(list[str]):
def __contains__(self, item: object) -> bool:
if not isinstance(item, str):
return False
for name in self:
if name == item.replace("-", "_").lower():
return True
return False
27 changes: 26 additions & 1 deletion api/src/opentrons/protocols/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import logging
import json
import os

from pathlib import Path
from typing import Any, AnyStr, Dict, Optional, Union
from typing import Any, AnyStr, Dict, Optional, Union, List

import jsonschema # type: ignore

from opentrons_shared_data import load_shared_data, get_shared_data_root
from opentrons.protocols.api_support.util import ModifiedList
from opentrons.protocols.api_support.constants import (
OPENTRONS_NAMESPACE,
CUSTOM_NAMESPACE,
Expand Down Expand Up @@ -61,6 +63,29 @@ def get_labware_definition(
return _get_standard_labware_definition(load_name, namespace, version)


def get_all_labware_definitions(schema_version: str = "2") -> List[str]:
"""
Return a list of standard and custom labware definitions with load_name +
name_space + version existing on the robot
"""
labware_list = ModifiedList()

def _check_for_subdirectories(path: Union[str, Path, os.DirEntry[str]]) -> None:
with os.scandir(path) as top_path:
for sub_dir in top_path:
if sub_dir.is_dir():
labware_list.append(sub_dir.name)

# check for standard labware
_check_for_subdirectories(
get_shared_data_root() / STANDARD_DEFS_PATH / schema_version
)
# check for custom labware
for namespace in os.scandir(USER_DEFS_PATH):
_check_for_subdirectories(namespace)
return labware_list


def save_definition(
labware_def: LabwareDefinition, force: bool = False, location: Optional[Path] = None
) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
NoResponse,
AlarmResponse,
ErrorResponse,
UnhandledGcode,
)


Expand Down Expand Up @@ -149,25 +150,31 @@ async def test_send_command_response(


@pytest.mark.parametrize(
argnames=["response", "exception_type"],
argnames=["response", "exception_type", "async_only"],
argvalues=[
["error", ErrorResponse],
["Error", ErrorResponse],
["Error: was found.", ErrorResponse],
["alarm", AlarmResponse],
["ALARM", AlarmResponse],
["This is an Alarm", AlarmResponse],
["error:Alarm lock", AlarmResponse],
["alarm:error", AlarmResponse],
["ALARM: Hard limit -X", AlarmResponse],
["error", ErrorResponse, False],
["Error", ErrorResponse, False],
["Error: was found.", ErrorResponse, False],
["alarm", AlarmResponse, False],
["ALARM", AlarmResponse, False],
["This is an Alarm", AlarmResponse, False],
["error:Alarm lock", AlarmResponse, False],
["alarm:error", AlarmResponse, False],
["ALARM: Hard limit -X", AlarmResponse, False],
["ERR003:unhandled gcode OK ", UnhandledGcode, True],
],
)
def test_raise_on_error(
subject: SerialKind, response: str, exception_type: Type[Exception]
subject: SerialKind,
response: str,
exception_type: Type[Exception],
async_only: bool,
) -> None:
"""It should raise an exception on error/alarm responses."""
if isinstance(subject, SerialConnection) and async_only:
pytest.skip()
with pytest.raises(expected_exception=exception_type, match=response):
subject.raise_on_error(response)
subject.raise_on_error(response, "fake request")


async def test_on_retry(mock_serial_port: AsyncMock, subject: SerialKind) -> None:
Expand All @@ -188,14 +195,15 @@ async def test_send_data_with_async_error_before(
serial_error_response = f" {error_response} {ack}"
encoded_error_response = serial_error_response.encode()
successful_response = "G28"
data = "G28"
serial_successful_response = f" {successful_response} {ack}"
encoded_successful_response = serial_successful_response.encode()
mock_serial_port.read_until.side_effect = [
encoded_error_response,
encoded_successful_response,
]

response = await subject_raise_on_error_patched._send_data(data="G28")
response = await subject_raise_on_error_patched._send_data(data=data)

assert response == successful_response
mock_serial_port.read_until.assert_has_calls(
Expand All @@ -206,8 +214,8 @@ async def test_send_data_with_async_error_before(
)
subject_raise_on_error_patched.raise_on_error.assert_has_calls( # type: ignore[attr-defined]
calls=[
call(response=error_response),
call(response=successful_response),
call(response=error_response, request=data),
call(response=successful_response, request=data),
]
)

Expand All @@ -222,14 +230,15 @@ async def test_send_data_with_async_error_after(
serial_error_response = f" {error_response} {ack}"
encoded_error_response = serial_error_response.encode()
successful_response = "G28"
data = "G28"
serial_successful_response = f" {successful_response} {ack}"
encoded_successful_response = serial_successful_response.encode()
mock_serial_port.read_until.side_effect = [
encoded_successful_response,
encoded_error_response,
]

response = await subject_raise_on_error_patched._send_data(data="G28")
response = await subject_raise_on_error_patched._send_data(data=data)

assert response == successful_response
mock_serial_port.read_until.assert_has_calls(
Expand All @@ -239,6 +248,6 @@ async def test_send_data_with_async_error_after(
)
subject_raise_on_error_patched.raise_on_error.assert_has_calls( # type: ignore[attr-defined]
calls=[
call(response=successful_response),
call(response=successful_response, request=data),
]
)
Loading

0 comments on commit bdc5788

Please sign in to comment.