-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* feat: Heater-Shaker Firmware Emulator remove files Fix some parsing errors Formatting Change rpm_per_tick * Tests * Modify home functionality Formatting and linting * fix rpm emulator g-code and add deactivate test * Fix tests * Add deactivate_heater G-Code * Add start_set_temperature * Update home and deactivate_heater functionality Based off of #11121 (comment) * Linting * fix: Fix deactivate_heater function * Make home_delay_time customizable * Linting Co-authored-by: jbleon95 <[email protected]> Co-authored-by: jbleon95 <[email protected]>
- Loading branch information
1 parent
30ee961
commit 0df9319
Showing
8 changed files
with
337 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
api/src/opentrons/hardware_control/emulation/heater_shaker.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
"""An emulation of the opentrons heater shaker module. | ||
The purpose is to provide a fake backend that responds to GCODE commands. | ||
""" | ||
import logging | ||
from time import sleep | ||
from typing import ( | ||
Optional, | ||
) | ||
|
||
from opentrons.drivers.heater_shaker.driver import ( | ||
GCODE, | ||
HS_ACK, | ||
) | ||
from opentrons.hardware_control.emulation.parser import Parser, Command | ||
from opentrons.hardware_control.emulation.settings import HeaterShakerSettings | ||
from . import util | ||
|
||
from .abstract_emulator import AbstractEmulator | ||
from .simulations import ( | ||
Temperature, | ||
RPM, | ||
) | ||
from .util import TEMPERATURE_ROOM | ||
from ...drivers.types import HeaterShakerLabwareLatchStatus | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class HeaterShakerEmulator(AbstractEmulator): | ||
"""Heater Shaker emulator""" | ||
|
||
_temperature: Temperature | ||
_rpm: RPM | ||
_latch_status: HeaterShakerLabwareLatchStatus | ||
|
||
def __init__(self, parser: Parser, settings: HeaterShakerSettings) -> None: | ||
self._parser = parser | ||
self._settings = settings | ||
self._gcode_to_function_mapping = { | ||
GCODE.SET_RPM.value: self._set_rpm, | ||
GCODE.GET_RPM.value: self._get_rpm, | ||
GCODE.SET_TEMPERATURE.value: self._set_temp, | ||
GCODE.GET_TEMPERATURE.value: self._get_temp, | ||
GCODE.HOME.value: self._home, | ||
GCODE.ENTER_BOOTLOADER.value: self._enter_bootloader, | ||
GCODE.GET_VERSION.value: self._get_version, | ||
GCODE.OPEN_LABWARE_LATCH.value: self._open_labware_latch, | ||
GCODE.CLOSE_LABWARE_LATCH.value: self._close_labware_latch, | ||
GCODE.GET_LABWARE_LATCH_STATE.value: self._get_labware_latch_state, | ||
GCODE.DEACTIVATE_HEATER.value: self._deactivate_heater, | ||
} | ||
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) -> None: | ||
|
||
self._temperature = Temperature( | ||
per_tick=self._settings.temperature.degrees_per_tick, | ||
current=self._settings.temperature.starting, | ||
) | ||
self._rpm = RPM( | ||
per_tick=self._settings.rpm.rpm_per_tick, | ||
current=self._settings.rpm.starting, | ||
) | ||
self._rpm.set_target(0.0) | ||
self._latch_status = HeaterShakerLabwareLatchStatus.IDLE_OPEN | ||
|
||
def _handle(self, command: Command) -> Optional[str]: | ||
""" | ||
Handle a command. | ||
TODO: AL 20210218 create dispatch map and remove 'noqa(C901)' | ||
""" | ||
logger.info(f"Got command {command}") | ||
func_to_run = self._gcode_to_function_mapping.get(command.gcode) | ||
res = None if func_to_run is None else func_to_run(command) | ||
return None if not isinstance(res, str) else f"{res} {HS_ACK}" | ||
|
||
def _set_rpm(self, command: Command) -> str: | ||
value = command.params["S"] | ||
assert isinstance(value, float), f"invalid value '{value}'" | ||
self._rpm.set_target(value) | ||
return "M3" | ||
|
||
def _get_rpm(self, command: Command) -> str: | ||
res = ( | ||
f"M123 C:{self._rpm.current} " | ||
f"T:{self._rpm.target if self._rpm.target is not None else 0}" | ||
) | ||
self._rpm.tick() | ||
return res | ||
|
||
def _set_temp(self, command: Command) -> str: | ||
value = command.params["S"] | ||
assert isinstance(value, float), f"invalid value '{value}'" | ||
self._temperature.set_target(value) | ||
return "M104" | ||
|
||
def _get_temp(self, command: Command) -> str: | ||
res = ( | ||
f"M105 C:{self._temperature.current} " | ||
f"T:{util.OptionalValue(self._temperature.target)}" | ||
) | ||
self._temperature.tick() | ||
return res | ||
|
||
def _home(self, command: Command) -> str: | ||
sleep(self._settings.home_delay_time) | ||
self._rpm.deactivate(0.0) | ||
self._rpm.set_target(0.0) | ||
return "G28" | ||
|
||
def _enter_bootloader(self, command: Command) -> None: | ||
pass | ||
|
||
def _get_version(self, command: Command) -> str: | ||
return ( | ||
f"FW:{self._settings.version} " | ||
f"HW:{self._settings.model} " | ||
f"SerialNo:{self._settings.serial_number}" | ||
) | ||
|
||
def _open_labware_latch(self, command: Command) -> str: | ||
self._latch_status = HeaterShakerLabwareLatchStatus.IDLE_OPEN | ||
return "M242" | ||
|
||
def _close_labware_latch(self, command: Command) -> str: | ||
self._latch_status = HeaterShakerLabwareLatchStatus.IDLE_CLOSED | ||
return "M243" | ||
|
||
def _get_labware_latch_state(self, command: Command) -> str: | ||
return f"M241 STATUS:{self._latch_status.value.upper()}" | ||
|
||
def _deactivate_heater(self, command: Command) -> str: | ||
self._temperature.deactivate(TEMPERATURE_ROOM) | ||
return "M106" | ||
|
||
@staticmethod | ||
def get_terminator() -> bytes: | ||
return b"\n" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
api/tests/opentrons/hardware_control/integration/test_heatershaker.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import asyncio | ||
from typing import AsyncIterator, Iterator | ||
|
||
import pytest | ||
from mock import AsyncMock | ||
from opentrons.drivers.rpi_drivers.types import USBPort | ||
from opentrons.hardware_control.emulation.settings import Settings | ||
from opentrons.hardware_control.emulation.util import TEMPERATURE_ROOM | ||
|
||
from opentrons.hardware_control.modules import HeaterShaker | ||
|
||
TEMP_ROOM_LOW = TEMPERATURE_ROOM - 0.7 | ||
TEMP_ROOM_HIGH = TEMPERATURE_ROOM + 0.7 | ||
|
||
|
||
@pytest.fixture | ||
async def heatershaker( | ||
emulation_app: Iterator[None], | ||
emulator_settings: Settings, | ||
) -> AsyncIterator[HeaterShaker]: | ||
module = await HeaterShaker.build( | ||
port=f"socket://127.0.0.1:{emulator_settings.heatershaker_proxy.driver_port}", | ||
execution_manager=AsyncMock(), | ||
usb_port=USBPort(name="", port_number=1, device_path="", hub=1), | ||
loop=asyncio.get_running_loop(), | ||
polling_period=0.5, | ||
) | ||
yield module | ||
await module.cleanup() | ||
|
||
|
||
def test_device_info(heatershaker: HeaterShaker): | ||
"""Confirm device_info returns correct values.""" | ||
assert heatershaker.device_info == { | ||
"model": "v01", | ||
"version": "v0.0.1", | ||
"serial": "heater_shaker_emulator", | ||
} | ||
|
||
|
||
async def test_latch_status(heatershaker: HeaterShaker) -> None: | ||
"""It should run open and close latch.""" | ||
await heatershaker.wait_next_poll() | ||
assert heatershaker.labware_latch_status.value == "idle_open" | ||
|
||
await heatershaker.close_labware_latch() | ||
assert heatershaker.labware_latch_status.value == "idle_closed" | ||
|
||
await heatershaker.open_labware_latch() | ||
assert heatershaker.labware_latch_status.value == "idle_open" | ||
|
||
|
||
async def test_speed(heatershaker: HeaterShaker) -> None: | ||
"""It should speed up, then slow down.""" | ||
|
||
await heatershaker.wait_next_poll() | ||
await heatershaker.set_speed(550) | ||
assert heatershaker.target_speed == 550 | ||
|
||
# The acceptable delta for actual speed is 100 | ||
assert 450 <= heatershaker.speed <= 650 | ||
|
||
|
||
async def test_deactivate_shaker(heatershaker: HeaterShaker) -> None: | ||
"""It should speed up, then slow down.""" | ||
|
||
await heatershaker.wait_next_poll() | ||
await heatershaker.set_speed(150) | ||
assert heatershaker.target_speed == 150 | ||
|
||
await heatershaker.deactivate_shaker() | ||
|
||
assert heatershaker.speed == 0 | ||
assert heatershaker.target_speed is None | ||
|
||
|
||
async def test_deactivate_heater(heatershaker: HeaterShaker) -> None: | ||
await heatershaker.wait_next_poll() | ||
await heatershaker.start_set_temperature(30.0) | ||
await heatershaker.await_temperature(30.0) | ||
assert heatershaker.target_temperature == 30.0 | ||
assert 29.3 <= heatershaker.temperature <= 30.7 | ||
|
||
await heatershaker.deactivate_heater() | ||
assert heatershaker.target_temperature is None | ||
assert TEMP_ROOM_LOW <= heatershaker.temperature <= TEMP_ROOM_HIGH | ||
|
||
|
||
async def test_temp(heatershaker: HeaterShaker) -> None: | ||
"""Test setting temp""" | ||
|
||
# Have to wait for next poll because target temp will not update until then | ||
await heatershaker.wait_next_poll() | ||
await heatershaker.start_set_temperature(50.0) | ||
assert heatershaker.target_temperature == 50.0 | ||
assert heatershaker.temperature != 50.0 | ||
|
||
await heatershaker.await_temperature(50.0) | ||
assert heatershaker.target_temperature == 50.0 | ||
|
||
# Acceptable delta is 0.7 degrees | ||
assert 49.3 <= heatershaker.temperature <= 50.7 |
Oops, something went wrong.