opentrons 8.7.0a5__py3-none-any.whl → 8.7.0a7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/_version.py +2 -2
- opentrons/drivers/asyncio/communication/serial_connection.py +129 -52
- opentrons/drivers/heater_shaker/abstract.py +5 -0
- opentrons/drivers/heater_shaker/driver.py +10 -0
- opentrons/drivers/heater_shaker/simulator.py +4 -0
- opentrons/drivers/thermocycler/abstract.py +6 -0
- opentrons/drivers/thermocycler/driver.py +61 -10
- opentrons/drivers/thermocycler/simulator.py +6 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/ot3controller.py +3 -0
- opentrons/hardware_control/backends/ot3simulator.py +2 -1
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
- opentrons/hardware_control/emulation/connection_handler.py +8 -5
- opentrons/hardware_control/emulation/heater_shaker.py +12 -3
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +67 -15
- opentrons/hardware_control/module_control.py +82 -8
- opentrons/hardware_control/modules/__init__.py +3 -0
- opentrons/hardware_control/modules/absorbance_reader.py +11 -4
- opentrons/hardware_control/modules/flex_stacker.py +38 -9
- opentrons/hardware_control/modules/heater_shaker.py +42 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +13 -5
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +68 -11
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +26 -5
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +31 -2
- opentrons/legacy_commands/module_commands.py +23 -0
- opentrons/legacy_commands/protocol_commands.py +20 -0
- opentrons/legacy_commands/types.py +80 -0
- opentrons/motion_planning/deck_conflict.py +17 -12
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -1
- opentrons/protocol_api/_types.py +6 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +75 -8
- opentrons/protocol_api/core/engine/protocol.py +18 -1
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +24 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +37 -4
- opentrons/protocol_api/core/protocol.py +11 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/labware.py +5 -0
- opentrons/protocol_api/module_contexts.py +117 -11
- opentrons/protocol_api/protocol_context.py +26 -4
- opentrons/protocol_api/robot_context.py +38 -21
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +6 -1
- opentrons/protocol_engine/actions/__init__.py +4 -2
- opentrons/protocol_engine/actions/actions.py +22 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +42 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/command.py +1 -0
- opentrons/protocol_engine/commands/command_unions.py +49 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
- opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
- opentrons/protocol_engine/commands/movement_common.py +2 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
- opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +40 -6
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +29 -5
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
- opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
- opentrons/protocol_engine/errors/__init__.py +4 -0
- opentrons/protocol_engine/errors/exceptions.py +55 -0
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +8 -0
- opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
- opentrons/protocol_engine/execution/labware_movement.py +9 -12
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/queue_worker.py +4 -0
- opentrons/protocol_engine/execution/run_control.py +8 -0
- opentrons/protocol_engine/execution/task_handler.py +157 -0
- opentrons/protocol_engine/protocol_engine.py +75 -34
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/commands.py +14 -11
- opentrons/protocol_engine/state/geometry.py +213 -374
- opentrons/protocol_engine/state/labware.py +52 -102
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +21 -8
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/state.py +14 -0
- opentrons/protocol_engine/state/state_summary.py +2 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +16 -9
- opentrons/protocol_engine/types/__init__.py +9 -3
- opentrons/protocol_engine/types/deck_configuration.py +5 -1
- opentrons/protocol_engine/types/instrument.py +8 -1
- opentrons/protocol_engine/types/labware.py +1 -13
- opentrons/protocol_engine/types/module.py +10 -0
- opentrons/protocol_engine/types/tasks.py +38 -0
- opentrons/protocol_engine/types/tip.py +9 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
- opentrons/protocol_runner/run_orchestrator.py +18 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +48 -15
- opentrons/system/camera.py +1 -1
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/RECORD +143 -127
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/licenses/LICENSE +0 -0
opentrons/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '8.7.
|
|
32
|
-
__version_tuple__ = version_tuple = (8, 7, 0, '
|
|
31
|
+
__version__ = version = '8.7.0a7'
|
|
32
|
+
__version_tuple__ = version_tuple = (8, 7, 0, 'a7')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Type, Literal, AsyncIterator
|
|
6
6
|
|
|
7
7
|
from opentrons.drivers.command_builder import CommandBuilder
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ class SerialConnection:
|
|
|
25
25
|
port: str,
|
|
26
26
|
baud_rate: int,
|
|
27
27
|
timeout: float,
|
|
28
|
-
loop:
|
|
28
|
+
loop: asyncio.AbstractEventLoop | None,
|
|
29
29
|
reset_buffer_before_write: bool,
|
|
30
30
|
) -> AsyncSerial:
|
|
31
31
|
return await AsyncSerial.create(
|
|
@@ -43,11 +43,11 @@ class SerialConnection:
|
|
|
43
43
|
baud_rate: int,
|
|
44
44
|
timeout: float,
|
|
45
45
|
ack: str,
|
|
46
|
-
name:
|
|
46
|
+
name: str | None = None,
|
|
47
47
|
retry_wait_time_seconds: float = 0.1,
|
|
48
|
-
loop:
|
|
49
|
-
error_keyword:
|
|
50
|
-
alarm_keyword:
|
|
48
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
49
|
+
error_keyword: str | None = None,
|
|
50
|
+
alarm_keyword: str | None = None,
|
|
51
51
|
reset_buffer_before_write: bool = False,
|
|
52
52
|
error_codes: Type[BaseErrorCode] = DefaultErrorCodes,
|
|
53
53
|
) -> "SerialConnection":
|
|
@@ -133,7 +133,7 @@ class SerialConnection:
|
|
|
133
133
|
self._error_codes = error_codes
|
|
134
134
|
|
|
135
135
|
async def send_command(
|
|
136
|
-
self, command: CommandBuilder, retries: int = 0, timeout:
|
|
136
|
+
self, command: CommandBuilder, retries: int = 0, timeout: float | None = None
|
|
137
137
|
) -> str:
|
|
138
138
|
"""
|
|
139
139
|
Send a command and return the response.
|
|
@@ -165,7 +165,7 @@ class SerialConnection:
|
|
|
165
165
|
await self._serial.write(data=encoded_command)
|
|
166
166
|
|
|
167
167
|
async def send_data(
|
|
168
|
-
self, data: str, retries: int = 0, timeout:
|
|
168
|
+
self, data: str, retries: int = 0, timeout: float | None = None
|
|
169
169
|
) -> str:
|
|
170
170
|
"""
|
|
171
171
|
Send data and return the response.
|
|
@@ -184,7 +184,7 @@ class SerialConnection:
|
|
|
184
184
|
):
|
|
185
185
|
return await self._send_data(data=data, retries=retries)
|
|
186
186
|
|
|
187
|
-
async def _send_data(self, data: str, retries: int
|
|
187
|
+
async def _send_data(self, data: str, retries: int) -> str:
|
|
188
188
|
"""
|
|
189
189
|
Send data and return the response.
|
|
190
190
|
|
|
@@ -351,14 +351,14 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
351
351
|
baud_rate: int,
|
|
352
352
|
timeout: float,
|
|
353
353
|
ack: str,
|
|
354
|
-
name:
|
|
354
|
+
name: str | None = None,
|
|
355
355
|
retry_wait_time_seconds: float = 0.1,
|
|
356
|
-
loop:
|
|
357
|
-
error_keyword:
|
|
358
|
-
alarm_keyword:
|
|
356
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
357
|
+
error_keyword: str | None = None,
|
|
358
|
+
alarm_keyword: str | None = None,
|
|
359
359
|
reset_buffer_before_write: bool = False,
|
|
360
360
|
error_codes: Type[BaseErrorCode] = DefaultErrorCodes,
|
|
361
|
-
async_error_ack:
|
|
361
|
+
async_error_ack: str | None = None,
|
|
362
362
|
number_of_retries: int = 0,
|
|
363
363
|
) -> AsyncResponseSerialConnection:
|
|
364
364
|
"""
|
|
@@ -461,8 +461,29 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
461
461
|
self._alarm_keyword = alarm_keyword.lower()
|
|
462
462
|
self._async_error_ack = async_error_ack.lower()
|
|
463
463
|
|
|
464
|
+
async def send_multiack_command(
|
|
465
|
+
self,
|
|
466
|
+
command: CommandBuilder,
|
|
467
|
+
retries: int = 0,
|
|
468
|
+
timeout: float | None = None,
|
|
469
|
+
acks: int = 1,
|
|
470
|
+
) -> list[str]:
|
|
471
|
+
"""Send a command and return the responses.
|
|
472
|
+
|
|
473
|
+
Some commands result in multiple responses; collate them and return them all.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
command: A command builder.
|
|
477
|
+
retries: number of times to retry in case of timeout
|
|
478
|
+
timeout: optional override of default timeout in seconds
|
|
479
|
+
acks: the number of acks to expect
|
|
480
|
+
"""
|
|
481
|
+
return await self.send_data_multiack(
|
|
482
|
+
data=command.build(), retries=retries, timeout=timeout, acks=acks
|
|
483
|
+
)
|
|
484
|
+
|
|
464
485
|
async def send_command(
|
|
465
|
-
self, command: CommandBuilder, retries: int = 0, timeout:
|
|
486
|
+
self, command: CommandBuilder, retries: int = 0, timeout: float | None = None
|
|
466
487
|
) -> str:
|
|
467
488
|
"""
|
|
468
489
|
Send a command and return the response.
|
|
@@ -482,8 +503,19 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
482
503
|
timeout=timeout,
|
|
483
504
|
)
|
|
484
505
|
|
|
506
|
+
async def send_data_multiack(
|
|
507
|
+
self, data: str, retries: int = 0, timeout: float | None = None, acks: int = 1
|
|
508
|
+
) -> list[str]:
|
|
509
|
+
"""Send data and return all responses."""
|
|
510
|
+
async with super().send_data_lock, self._serial.timeout_override(
|
|
511
|
+
"timeout", timeout
|
|
512
|
+
):
|
|
513
|
+
return await self._send_data_multiack(
|
|
514
|
+
data=data, retries=retries or self._number_of_retries, acks=acks
|
|
515
|
+
)
|
|
516
|
+
|
|
485
517
|
async def send_data(
|
|
486
|
-
self, data: str, retries: int = 0, timeout:
|
|
518
|
+
self, data: str, retries: int = 0, timeout: float | None = None
|
|
487
519
|
) -> str:
|
|
488
520
|
"""
|
|
489
521
|
Send data and return the response.
|
|
@@ -504,54 +536,99 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
504
536
|
data=data, retries=retries or self._number_of_retries
|
|
505
537
|
)
|
|
506
538
|
|
|
507
|
-
async def
|
|
539
|
+
async def _consume_responses(
|
|
540
|
+
self, acks: int
|
|
541
|
+
) -> AsyncIterator[tuple[Literal["response", "error", "empty-unknown"], bytes]]:
|
|
542
|
+
while acks > 0:
|
|
543
|
+
data = await self._serial.read_until(match=self._ack)
|
|
544
|
+
log.debug(f"{self._name}: Read <- {data!r}")
|
|
545
|
+
if self._async_error_ack.encode() in data:
|
|
546
|
+
yield "error", data
|
|
547
|
+
elif self._ack in data:
|
|
548
|
+
yield "response", data
|
|
549
|
+
acks -= 1
|
|
550
|
+
else:
|
|
551
|
+
# A read timeout, end
|
|
552
|
+
yield "empty-unknown", data
|
|
553
|
+
|
|
554
|
+
async def _send_one_retry(self, data: str, acks: int) -> list[str]:
|
|
555
|
+
data_encode = data.encode("utf-8")
|
|
556
|
+
log.debug(f"{self._name}: Write -> {data_encode!r}")
|
|
557
|
+
await self._serial.write(data=data_encode)
|
|
558
|
+
|
|
559
|
+
command_acks: list[bytes] = []
|
|
560
|
+
async_errors: list[bytes] = []
|
|
561
|
+
# consume responses before raising so we don't raise and orphan
|
|
562
|
+
# a response in the buffer
|
|
563
|
+
async for response_type, response in self._consume_responses(acks):
|
|
564
|
+
if response_type == "error":
|
|
565
|
+
async_errors.append(response)
|
|
566
|
+
elif response_type == "response":
|
|
567
|
+
command_acks.append(response)
|
|
568
|
+
else:
|
|
569
|
+
break
|
|
570
|
+
|
|
571
|
+
for async_error in async_errors:
|
|
572
|
+
# Remove ack from response
|
|
573
|
+
ackless_response = async_error.replace(self._ack, b"")
|
|
574
|
+
str_response = self.process_raw_response(
|
|
575
|
+
command=data, response=ackless_response.decode()
|
|
576
|
+
)
|
|
577
|
+
self.raise_on_error(response=str_response, request=data)
|
|
578
|
+
|
|
579
|
+
ackless_responses: list[str] = []
|
|
580
|
+
for command_ack in command_acks:
|
|
581
|
+
# Remove ack from response
|
|
582
|
+
ackless_response = command_ack.replace(self._ack, b"")
|
|
583
|
+
str_response = self.process_raw_response(
|
|
584
|
+
command=data, response=ackless_response.decode()
|
|
585
|
+
)
|
|
586
|
+
self.raise_on_error(response=str_response, request=data)
|
|
587
|
+
ackless_responses.append(str_response)
|
|
588
|
+
return ackless_responses
|
|
589
|
+
|
|
590
|
+
async def _send_data_multiack(
|
|
591
|
+
self, data: str, retries: int, acks: int
|
|
592
|
+
) -> list[str]:
|
|
508
593
|
"""
|
|
509
|
-
Send data and return the response.
|
|
594
|
+
Send data and return the response(s).
|
|
510
595
|
|
|
511
596
|
Args:
|
|
512
597
|
data: The data to send.
|
|
513
598
|
retries: number of times to retry in case of timeout
|
|
599
|
+
acks: The number of expected command responses
|
|
514
600
|
|
|
515
|
-
|
|
601
|
+
This function retries (resends the command) up to (retries) times, and waits
|
|
602
|
+
for (acks) responses. It also listens for async errors. These are an older
|
|
603
|
+
mechanism where at the moment an error occurs, some modules will send a message
|
|
604
|
+
like async error ERR:202:whatever
|
|
516
605
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
retries = retries or self._number_of_retries
|
|
606
|
+
This function will detect async error messages if they were sent before it
|
|
607
|
+
sent the command or if they are sent before the final ack for the command is
|
|
608
|
+
sent. It will not catch async errors otherwise.
|
|
521
609
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
await self._serial.write(data=data_encode)
|
|
610
|
+
This function will always try and consume all the acknowledgements specified for
|
|
611
|
+
its command if it sends the command, even if an async error happens in between.
|
|
525
612
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
while self._async_error_ack.encode() in response[-1].lower():
|
|
531
|
-
# check for multiple a priori async errors
|
|
532
|
-
response.append(await self._serial.read_until(match=self._ack))
|
|
533
|
-
log.debug(f"{self._name}: Read <- {response[-1]!r}")
|
|
534
|
-
|
|
535
|
-
for r in response:
|
|
536
|
-
if self._async_error_ack.encode() in r:
|
|
537
|
-
# Remove ack from response
|
|
538
|
-
ackless_response = r.replace(self._ack, b"")
|
|
539
|
-
str_response = self.process_raw_response(
|
|
540
|
-
command=data, response=ackless_response.decode()
|
|
541
|
-
)
|
|
542
|
-
self.raise_on_error(response=str_response, request=data)
|
|
613
|
+
This should all work together to make sure that there aren't any leftover acks
|
|
614
|
+
after the function ends, which could lead to the read/write mechanics getting out
|
|
615
|
+
of sync.
|
|
543
616
|
|
|
544
|
-
|
|
545
|
-
# Remove ack from response
|
|
546
|
-
ackless_response = response[-1].replace(self._ack, b"")
|
|
547
|
-
str_response = self.process_raw_response(
|
|
548
|
-
command=data, response=ackless_response.decode()
|
|
549
|
-
)
|
|
550
|
-
self.raise_on_error(response=str_response, request=data)
|
|
551
|
-
return str_response
|
|
617
|
+
Returns: The command responses
|
|
552
618
|
|
|
553
|
-
|
|
619
|
+
Raises: SerialException from an error ack to this command or an async error.
|
|
620
|
+
"""
|
|
621
|
+
retries = retries or self._number_of_retries
|
|
622
|
+
responses: list[str] = []
|
|
554
623
|
|
|
624
|
+
for retry in range(retries + 1):
|
|
625
|
+
responses = await self._send_one_retry(data, acks)
|
|
626
|
+
if responses:
|
|
627
|
+
return responses
|
|
628
|
+
log.info(f"{self._name}: retry number {retry}/{retries}")
|
|
555
629
|
await self.on_retry()
|
|
556
630
|
|
|
557
631
|
raise NoResponse(port=self._port, command=data)
|
|
632
|
+
|
|
633
|
+
async def _send_data(self, data: str, retries: int) -> str:
|
|
634
|
+
return (await self._send_data_multiack(data, retries, 1))[0]
|
|
@@ -27,6 +27,7 @@ class GCODE(str, Enum):
|
|
|
27
27
|
GET_LABWARE_LATCH_STATE = "M241"
|
|
28
28
|
DEACTIVATE_HEATER = "M106"
|
|
29
29
|
GET_RESET_REASON = "M114"
|
|
30
|
+
GET_ERROR_STATE = "M411"
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
HS_BAUDRATE = 115200
|
|
@@ -202,3 +203,12 @@ class HeaterShakerDriver(AbstractHeaterShakerDriver):
|
|
|
202
203
|
gcode=GCODE.DEACTIVATE_HEATER
|
|
203
204
|
)
|
|
204
205
|
await self._connection.send_command(command=c, retries=DEFAULT_COMMAND_RETRIES)
|
|
206
|
+
|
|
207
|
+
async def get_error_state(self) -> None:
|
|
208
|
+
"""Raise if the module is in an error state."""
|
|
209
|
+
await self._connection.send_multiack_command(
|
|
210
|
+
command=CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode(
|
|
211
|
+
gcode=GCODE.GET_ERROR_STATE
|
|
212
|
+
),
|
|
213
|
+
acks=2,
|
|
214
|
+
)
|
|
@@ -55,6 +55,7 @@ class AbstractThermocyclerDriver(ABC):
|
|
|
55
55
|
temp: float,
|
|
56
56
|
hold_time: Optional[float] = None,
|
|
57
57
|
volume: Optional[float] = None,
|
|
58
|
+
ramp_rate: Optional[float] = None,
|
|
58
59
|
) -> None:
|
|
59
60
|
"""Send set plate temperature command"""
|
|
60
61
|
...
|
|
@@ -97,3 +98,8 @@ class AbstractThermocyclerDriver(ABC):
|
|
|
97
98
|
async def jog_lid(self, angle: float) -> None:
|
|
98
99
|
"""Send the Jog Lid command."""
|
|
99
100
|
...
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
async def get_error_state(self) -> None:
|
|
104
|
+
"""Raise if the thermocycler is in an error state."""
|
|
105
|
+
...
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import Optional, Dict, Union
|
|
6
|
+
from typing import Optional, Dict, Union, TypeVar, Generic
|
|
7
7
|
|
|
8
8
|
from opentrons.drivers import utils
|
|
9
9
|
from opentrons.drivers.command_builder import CommandBuilder
|
|
@@ -36,6 +36,7 @@ class GCODE(str, Enum):
|
|
|
36
36
|
DEVICE_INFO = "M115"
|
|
37
37
|
GET_RESET_REASON = "M114"
|
|
38
38
|
ENTER_PROGRAMMING = "dfu"
|
|
39
|
+
GET_ERROR_STATE = "M411"
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
LID_TARGET_DEFAULT = 105 # Degree celsius (floats)
|
|
@@ -73,7 +74,7 @@ class ThermocyclerDriverFactory:
|
|
|
73
74
|
@staticmethod
|
|
74
75
|
async def create(
|
|
75
76
|
port: str, loop: Optional[asyncio.AbstractEventLoop]
|
|
76
|
-
) -> ThermocyclerDriver:
|
|
77
|
+
) -> ThermocyclerDriver | ThermocyclerDriverV2:
|
|
77
78
|
"""
|
|
78
79
|
Create a thermocycler driver.
|
|
79
80
|
|
|
@@ -148,10 +149,15 @@ class ThermocyclerDriverFactory:
|
|
|
148
149
|
return response.startswith(GCODE.DEVICE_INFO)
|
|
149
150
|
|
|
150
151
|
|
|
151
|
-
|
|
152
|
+
_ConnectionKind = TypeVar(
|
|
153
|
+
"_ConnectionKind", SerialConnection, AsyncResponseSerialConnection
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class _BaseThermocyclerDriver(AbstractThermocyclerDriver, Generic[_ConnectionKind]):
|
|
152
158
|
def __init__(
|
|
153
159
|
self,
|
|
154
|
-
connection:
|
|
160
|
+
connection: _ConnectionKind,
|
|
155
161
|
) -> None:
|
|
156
162
|
"""
|
|
157
163
|
Constructor
|
|
@@ -159,7 +165,7 @@ class ThermocyclerDriver(AbstractThermocyclerDriver):
|
|
|
159
165
|
Args:
|
|
160
166
|
connection: SerialConnection to the thermocycler
|
|
161
167
|
"""
|
|
162
|
-
self._connection = connection
|
|
168
|
+
self._connection: _ConnectionKind = connection
|
|
163
169
|
|
|
164
170
|
async def connect(self) -> None:
|
|
165
171
|
"""Connect to thermocycler"""
|
|
@@ -226,6 +232,7 @@ class ThermocyclerDriver(AbstractThermocyclerDriver):
|
|
|
226
232
|
temp: float,
|
|
227
233
|
hold_time: Optional[float] = None,
|
|
228
234
|
volume: Optional[float] = None,
|
|
235
|
+
ramp_rate: Optional[float] = None,
|
|
229
236
|
) -> None:
|
|
230
237
|
"""Send set plate temperature command"""
|
|
231
238
|
temp = min(BLOCK_TARGET_MAX, max(BLOCK_TARGET_MIN, temp))
|
|
@@ -328,8 +335,15 @@ class ThermocyclerDriver(AbstractThermocyclerDriver):
|
|
|
328
335
|
"Gen1 Thermocyclers do not support the Jog Lid command."
|
|
329
336
|
)
|
|
330
337
|
|
|
338
|
+
async def get_error_state(self) -> None:
|
|
339
|
+
"""For the gen1, do nothing."""
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
ThermocyclerDriver = _BaseThermocyclerDriver[SerialConnection]
|
|
331
344
|
|
|
332
|
-
|
|
345
|
+
|
|
346
|
+
class ThermocyclerDriverV2(_BaseThermocyclerDriver[AsyncResponseSerialConnection]):
|
|
333
347
|
"""
|
|
334
348
|
This driver is for Thermocycler model Gen2.
|
|
335
349
|
"""
|
|
@@ -343,10 +357,38 @@ class ThermocyclerDriverV2(ThermocyclerDriver):
|
|
|
343
357
|
"""
|
|
344
358
|
super().__init__(connection)
|
|
345
359
|
|
|
346
|
-
async def
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
360
|
+
async def set_plate_temperature(
|
|
361
|
+
self,
|
|
362
|
+
temp: float,
|
|
363
|
+
hold_time: Optional[float] = None,
|
|
364
|
+
volume: Optional[float] = None,
|
|
365
|
+
ramp_rate: Optional[float] = None,
|
|
366
|
+
) -> None:
|
|
367
|
+
"""Send set plate temperature command"""
|
|
368
|
+
temp = min(BLOCK_TARGET_MAX, max(BLOCK_TARGET_MIN, temp))
|
|
369
|
+
|
|
370
|
+
c = (
|
|
371
|
+
CommandBuilder(terminator=TC_COMMAND_TERMINATOR)
|
|
372
|
+
.add_gcode(gcode=GCODE.SET_PLATE_TEMP)
|
|
373
|
+
.add_float(
|
|
374
|
+
prefix="S", value=temp, precision=utils.TC_GCODE_ROUNDING_PRECISION
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
if hold_time is not None:
|
|
378
|
+
c = c.add_float(
|
|
379
|
+
prefix="H", value=hold_time, precision=utils.TC_GCODE_ROUNDING_PRECISION
|
|
380
|
+
)
|
|
381
|
+
if volume is not None:
|
|
382
|
+
c = c.add_float(
|
|
383
|
+
prefix="V", value=volume, precision=utils.TC_GCODE_ROUNDING_PRECISION
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if ramp_rate is not None:
|
|
387
|
+
c = c.add_float(
|
|
388
|
+
prefix="R", value=ramp_rate, precision=utils.TC_GCODE_ROUNDING_PRECISION
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
await self._connection.send_command(command=c, retries=DEFAULT_COMMAND_RETRIES)
|
|
350
392
|
|
|
351
393
|
async def get_device_info(self) -> Dict[str, str]:
|
|
352
394
|
"""Send get device info command"""
|
|
@@ -393,3 +435,12 @@ class ThermocyclerDriverV2(ThermocyclerDriver):
|
|
|
393
435
|
.add_element("O")
|
|
394
436
|
)
|
|
395
437
|
await self._connection.send_command(command=c, retries=1)
|
|
438
|
+
|
|
439
|
+
async def get_error_state(self) -> None:
|
|
440
|
+
"""Raise an error if the thermocycler is stuck in an error state."""
|
|
441
|
+
await self._connection.send_multiack_command(
|
|
442
|
+
command=CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode(
|
|
443
|
+
gcode=GCODE.GET_ERROR_STATE
|
|
444
|
+
),
|
|
445
|
+
acks=2,
|
|
446
|
+
)
|
|
@@ -67,10 +67,12 @@ class SimulatingDriver(AbstractThermocyclerDriver):
|
|
|
67
67
|
temp: float,
|
|
68
68
|
hold_time: Optional[float] = None,
|
|
69
69
|
volume: Optional[float] = None,
|
|
70
|
+
ramp_rate: Optional[float] = None,
|
|
70
71
|
) -> None:
|
|
71
72
|
self._plate_temperature.target = temp
|
|
72
73
|
self._plate_temperature.current = temp
|
|
73
74
|
self._plate_temperature.hold = 0
|
|
75
|
+
self._ramp_rate = ramp_rate
|
|
74
76
|
|
|
75
77
|
@ensure_yield
|
|
76
78
|
async def get_plate_temperature(self) -> PlateTemperature:
|
|
@@ -124,3 +126,7 @@ class SimulatingDriver(AbstractThermocyclerDriver):
|
|
|
124
126
|
if angle < 0
|
|
125
127
|
else ThermocyclerLidStatus.OPEN
|
|
126
128
|
)
|
|
129
|
+
|
|
130
|
+
@ensure_yield
|
|
131
|
+
async def get_error_state(self) -> None:
|
|
132
|
+
return
|
|
@@ -44,6 +44,7 @@ from .execution_manager import ExecutionManagerProvider
|
|
|
44
44
|
from .pause_manager import PauseManager
|
|
45
45
|
from .module_control import AttachedModulesControl
|
|
46
46
|
from .types import (
|
|
47
|
+
AsynchronousModuleErrorNotification,
|
|
47
48
|
Axis,
|
|
48
49
|
CriticalPoint,
|
|
49
50
|
DoorState,
|
|
@@ -51,6 +52,7 @@ from .types import (
|
|
|
51
52
|
ErrorMessageNotification,
|
|
52
53
|
HardwareEventHandler,
|
|
53
54
|
HardwareAction,
|
|
55
|
+
HardwareEvent,
|
|
54
56
|
MotionChecks,
|
|
55
57
|
PauseType,
|
|
56
58
|
StatusBarState,
|
|
@@ -167,6 +169,18 @@ class API(
|
|
|
167
169
|
except Exception:
|
|
168
170
|
mod_log.exception("Errored during door state event callback")
|
|
169
171
|
|
|
172
|
+
def _send_module_notification(self, event: HardwareEvent) -> None:
|
|
173
|
+
if not isinstance(event, AsynchronousModuleErrorNotification):
|
|
174
|
+
return
|
|
175
|
+
mod_log.info(
|
|
176
|
+
f"Forwarding module event {event.event} for {event.module_model} {event.module_serial} at {event.port}"
|
|
177
|
+
)
|
|
178
|
+
for cb in self._callbacks:
|
|
179
|
+
try:
|
|
180
|
+
cb(event)
|
|
181
|
+
except Exception:
|
|
182
|
+
mod_log.exception("Errored during module asynchronous callback")
|
|
183
|
+
|
|
170
184
|
def _reset_last_mount(self) -> None:
|
|
171
185
|
self._last_moved_mount = None
|
|
172
186
|
|
|
@@ -247,7 +261,9 @@ class API(
|
|
|
247
261
|
)
|
|
248
262
|
await api_instance.cache_instruments()
|
|
249
263
|
module_controls = await AttachedModulesControl.build(
|
|
250
|
-
api_instance,
|
|
264
|
+
api_instance,
|
|
265
|
+
board_revision=backend.board_revision,
|
|
266
|
+
event_callback=api_instance._send_module_notification,
|
|
251
267
|
)
|
|
252
268
|
backend.module_controls = module_controls
|
|
253
269
|
checked_loop.create_task(backend.watch(loop=checked_loop))
|
|
@@ -306,7 +322,9 @@ class API(
|
|
|
306
322
|
)
|
|
307
323
|
await api_instance.cache_instruments()
|
|
308
324
|
module_controls = await AttachedModulesControl.build(
|
|
309
|
-
api_instance,
|
|
325
|
+
api_instance,
|
|
326
|
+
board_revision=backend.board_revision,
|
|
327
|
+
event_callback=api_instance._send_module_notification,
|
|
310
328
|
)
|
|
311
329
|
backend.module_controls = module_controls
|
|
312
330
|
await backend.watch()
|
|
@@ -1312,9 +1330,10 @@ class API(
|
|
|
1312
1330
|
self.is_simulator
|
|
1313
1331
|
), "Cannot build simulating module from non-simulating hardware control API"
|
|
1314
1332
|
|
|
1315
|
-
return await self._backend.module_controls.
|
|
1316
|
-
|
|
1317
|
-
|
|
1333
|
+
return await self._backend.module_controls.register_simulated_module(
|
|
1334
|
+
simulated_usb_port=USBPort(
|
|
1335
|
+
name="", port_number=1, port_group=PortGroup.MAIN
|
|
1336
|
+
),
|
|
1318
1337
|
type=modules.ModuleType.from_model(model),
|
|
1319
1338
|
sim_model=model.value,
|
|
1320
1339
|
)
|
|
@@ -360,13 +360,19 @@ class Controller:
|
|
|
360
360
|
"""Run a probe and return the new position dict"""
|
|
361
361
|
return await self._smoothie_driver.probe_axis(axis, distance)
|
|
362
362
|
|
|
363
|
-
async def clean_up(self) -> None:
|
|
363
|
+
async def clean_up(self) -> None: # noqa: C901
|
|
364
364
|
try:
|
|
365
365
|
loop = asyncio.get_event_loop()
|
|
366
366
|
except RuntimeError:
|
|
367
367
|
return
|
|
368
|
+
if hasattr(self, "_module_controls") and self._module_controls is not None:
|
|
369
|
+
await self._module_controls.clean_up()
|
|
368
370
|
if hasattr(self, "_event_watcher"):
|
|
369
|
-
if
|
|
371
|
+
if (
|
|
372
|
+
loop.is_running()
|
|
373
|
+
and self._event_watcher
|
|
374
|
+
and not self._event_watcher.closed
|
|
375
|
+
):
|
|
370
376
|
self._event_watcher.close()
|
|
371
377
|
if hasattr(self, "gpio_chardev"):
|
|
372
378
|
try:
|
|
@@ -1398,6 +1398,9 @@ class OT3Controller(FlexBackend):
|
|
|
1398
1398
|
except RuntimeError:
|
|
1399
1399
|
return
|
|
1400
1400
|
|
|
1401
|
+
if hasattr(self, "_module_controls") and self._module_controls is not None:
|
|
1402
|
+
await self._module_controls.clean_up()
|
|
1403
|
+
|
|
1401
1404
|
if hasattr(self, "_event_watcher"):
|
|
1402
1405
|
if (
|
|
1403
1406
|
loop.is_running()
|
|
@@ -728,7 +728,8 @@ class OT3Simulator(FlexBackend):
|
|
|
728
728
|
@ensure_yield
|
|
729
729
|
async def clean_up(self) -> None:
|
|
730
730
|
"""Clean up."""
|
|
731
|
-
|
|
731
|
+
if hasattr(self, "_module_controls") and self._module_controls is not None:
|
|
732
|
+
await self._module_controls.clean_up()
|
|
732
733
|
|
|
733
734
|
@staticmethod
|
|
734
735
|
def _get_home_position() -> Dict[Axis, float]:
|
|
@@ -414,7 +414,8 @@ class Simulator:
|
|
|
414
414
|
|
|
415
415
|
@ensure_yield
|
|
416
416
|
async def clean_up(self) -> None:
|
|
417
|
-
|
|
417
|
+
if hasattr(self, "_module_controls") and self._module_controls is not None:
|
|
418
|
+
await self._module_controls.clean_up()
|
|
418
419
|
|
|
419
420
|
@ensure_yield
|
|
420
421
|
async def configure_mount(
|
|
@@ -91,6 +91,8 @@ class SubsystemManager:
|
|
|
91
91
|
self._present_tools = tools.types.ToolSummary(
|
|
92
92
|
left=None, right=None, gripper=None
|
|
93
93
|
)
|
|
94
|
+
# This is intended to be an internal variable but is modified in unit tests to avoid long timeouts
|
|
95
|
+
self._check_device_update_timeout = 10.0
|
|
94
96
|
|
|
95
97
|
@property
|
|
96
98
|
def ok(self) -> bool:
|
|
@@ -183,11 +185,12 @@ class SubsystemManager:
|
|
|
183
185
|
return self._tool_task_state is True
|
|
184
186
|
|
|
185
187
|
async def _check_devices_after_update(
|
|
186
|
-
self, devices: Set[SubSystem], timeout_sec: float =
|
|
188
|
+
self, devices: Set[SubSystem], timeout_sec: Optional[float] = None
|
|
187
189
|
) -> None:
|
|
188
190
|
try:
|
|
189
191
|
await asyncio.wait_for(
|
|
190
|
-
self._do_check_devices_after_update(devices),
|
|
192
|
+
self._do_check_devices_after_update(devices),
|
|
193
|
+
timeout=timeout_sec or self._check_device_update_timeout,
|
|
191
194
|
)
|
|
192
195
|
except asyncio.TimeoutError:
|
|
193
196
|
raise RuntimeError("Device failed to come back after firmware update")
|
|
@@ -10,12 +10,14 @@ class AbstractEmulator(ABC):
|
|
|
10
10
|
"""Handle a command and return a response."""
|
|
11
11
|
...
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
def get_terminator() -> bytes:
|
|
13
|
+
def get_terminator(self) -> bytes:
|
|
15
14
|
"""Get the command terminator for messages coming from PI."""
|
|
16
15
|
return b"\r\n\r\n"
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
def get_ack() -> bytes:
|
|
17
|
+
def get_ack(self) -> bytes:
|
|
20
18
|
"""Get the command ack send to the PI."""
|
|
21
19
|
return b"ok\r\nok\r\n"
|
|
20
|
+
|
|
21
|
+
def get_autoack(self) -> bool:
|
|
22
|
+
"""Should this system automatically acknowledge messages?"""
|
|
23
|
+
return True
|