opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__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.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +4 -1
- opentrons/config/__init__.py +7 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
- 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/drivers/vacuum_module/__init__.py +5 -0
- opentrons/drivers/vacuum_module/abstract.py +93 -0
- opentrons/drivers/vacuum_module/driver.py +208 -0
- opentrons/drivers/vacuum_module/errors.py +39 -0
- opentrons/drivers/vacuum_module/simulator.py +85 -0
- opentrons/drivers/vacuum_module/types.py +79 -0
- opentrons/execute.py +3 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +3 -1
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- 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 +105 -10
- 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 +14 -6
- 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/motion_utilities.py +6 -6
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +92 -17
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/protocols/liquid_handler.py +12 -4
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +43 -2
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +52 -0
- opentrons/legacy_commands/protocol_commands.py +53 -1
- opentrons/legacy_commands/types.py +155 -1
- 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/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +8 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +95 -4
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +4 -18
- opentrons/protocol_api/core/engine/protocol.py +51 -2
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/instrument.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -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_instrument_core.py +19 -2
- 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 +58 -2
- opentrons/protocol_api/core/protocol.py +23 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +10 -2
- opentrons/protocol_api/module_contexts.py +170 -6
- opentrons/protocol_api/protocol_context.py +87 -21
- opentrons/protocol_api/robot_context.py +41 -25
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +49 -3
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +6 -2
- opentrons/protocol_engine/actions/actions.py +31 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +56 -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/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
- opentrons/protocol_engine/commands/capture_image.py +302 -0
- opentrons/protocol_engine/commands/command.py +2 -0
- opentrons/protocol_engine/commands/command_unions.py +62 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
- 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/move_labware.py +3 -4
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
- opentrons/protocol_engine/commands/movement_common.py +31 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- 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 +44 -7
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
- 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/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +12 -0
- opentrons/protocol_engine/errors/exceptions.py +119 -0
- opentrons/protocol_engine/execution/__init__.py +4 -0
- opentrons/protocol_engine/execution/command_executor.py +62 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
- opentrons/protocol_engine/execution/labware_movement.py +13 -15
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/pipetting.py +26 -25
- 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 +137 -36
- opentrons/protocol_engine/resources/__init__.py +4 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +37 -14
- opentrons/protocol_engine/state/geometry.py +276 -379
- opentrons/protocol_engine/state/labware.py +62 -108
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +30 -8
- opentrons/protocol_engine/state/motion.py +60 -18
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +44 -0
- opentrons/protocol_engine/state/state_summary.py +4 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +26 -9
- opentrons/protocol_engine/types/__init__.py +23 -4
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- 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/location.py +26 -2
- opentrons/protocol_engine/types/module.py +11 -1
- 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/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +49 -2
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +51 -15
- opentrons/system/camera.py +334 -4
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/licenses/LICENSE +0 -0
|
@@ -52,6 +52,7 @@ from opentrons_shared_data.errors.exceptions import (
|
|
|
52
52
|
InvalidActuator,
|
|
53
53
|
FirmwareUpdateFailedError,
|
|
54
54
|
PipetteLiquidNotFoundError,
|
|
55
|
+
PipetteOverpressureError,
|
|
55
56
|
)
|
|
56
57
|
|
|
57
58
|
from .util import use_or_initialize_loop, check_motion_bounds
|
|
@@ -74,6 +75,8 @@ from .types import (
|
|
|
74
75
|
DoorStateNotification,
|
|
75
76
|
ErrorMessageNotification,
|
|
76
77
|
HardwareEvent,
|
|
78
|
+
AsynchronousModuleErrorNotification,
|
|
79
|
+
ModuleDisconnectedNotification,
|
|
77
80
|
HardwareEventHandler,
|
|
78
81
|
HardwareAction,
|
|
79
82
|
HepaFanState,
|
|
@@ -367,6 +370,24 @@ class OT3API(
|
|
|
367
370
|
|
|
368
371
|
return futures
|
|
369
372
|
|
|
373
|
+
def _send_module_notification(self, event: HardwareEvent) -> None:
|
|
374
|
+
if not isinstance(
|
|
375
|
+
event,
|
|
376
|
+
(
|
|
377
|
+
AsynchronousModuleErrorNotification,
|
|
378
|
+
ModuleDisconnectedNotification,
|
|
379
|
+
),
|
|
380
|
+
):
|
|
381
|
+
return
|
|
382
|
+
mod_log.info(
|
|
383
|
+
f"Forwarding module event {event.event} for {event.module_model} {event.module_serial} at {event.port}"
|
|
384
|
+
)
|
|
385
|
+
for cb in self._callbacks:
|
|
386
|
+
try:
|
|
387
|
+
cb(event)
|
|
388
|
+
except Exception:
|
|
389
|
+
mod_log.exception("Errored during module asynchronous callback")
|
|
390
|
+
|
|
370
391
|
def _reset_last_mount(self) -> None:
|
|
371
392
|
self._last_moved_mount = None
|
|
372
393
|
|
|
@@ -422,7 +443,9 @@ class OT3API(
|
|
|
422
443
|
|
|
423
444
|
await api_instance.set_status_bar_enabled(status_bar_enabled)
|
|
424
445
|
module_controls = await AttachedModulesControl.build(
|
|
425
|
-
api_instance,
|
|
446
|
+
api_instance,
|
|
447
|
+
board_revision=backend.board_revision,
|
|
448
|
+
event_callback=api_instance._send_module_notification,
|
|
426
449
|
)
|
|
427
450
|
backend.module_controls = module_controls
|
|
428
451
|
await backend.build_estop_detector()
|
|
@@ -484,7 +507,9 @@ class OT3API(
|
|
|
484
507
|
)
|
|
485
508
|
await api_instance.cache_instruments()
|
|
486
509
|
module_controls = await AttachedModulesControl.build(
|
|
487
|
-
api_instance,
|
|
510
|
+
api_instance,
|
|
511
|
+
board_revision=backend.board_revision,
|
|
512
|
+
event_callback=api_instance._send_module_notification,
|
|
488
513
|
)
|
|
489
514
|
backend.module_controls = module_controls
|
|
490
515
|
await backend.watch(api_instance.loop)
|
|
@@ -627,9 +652,10 @@ class OT3API(
|
|
|
627
652
|
self.is_simulator
|
|
628
653
|
), "Cannot build simulating module from non-simulating hardware control API"
|
|
629
654
|
|
|
630
|
-
return await self._backend.module_controls.
|
|
631
|
-
|
|
632
|
-
|
|
655
|
+
return await self._backend.module_controls.register_simulated_module(
|
|
656
|
+
simulated_usb_port=USBPort(
|
|
657
|
+
name="", port_number=1, port_group=PortGroup.LEFT
|
|
658
|
+
),
|
|
633
659
|
type=modules.ModuleType.from_model(model),
|
|
634
660
|
sim_model=model.value,
|
|
635
661
|
)
|
|
@@ -1499,6 +1525,7 @@ class OT3API(
|
|
|
1499
1525
|
acquire_lock: bool = True,
|
|
1500
1526
|
check_bounds: MotionChecks = MotionChecks.NONE,
|
|
1501
1527
|
expect_stalls: bool = False,
|
|
1528
|
+
delay: Optional[Tuple[List[Axis], float]] = None,
|
|
1502
1529
|
) -> None:
|
|
1503
1530
|
"""Worker function to apply robot motion."""
|
|
1504
1531
|
machine_pos = machine_from_deck(
|
|
@@ -1532,6 +1559,7 @@ class OT3API(
|
|
|
1532
1559
|
machine_pos,
|
|
1533
1560
|
speed or 400.0,
|
|
1534
1561
|
HWStopCondition.stall if expect_stalls else HWStopCondition.none,
|
|
1562
|
+
delay=delay,
|
|
1535
1563
|
)
|
|
1536
1564
|
except Exception:
|
|
1537
1565
|
self._log.exception("Move failed")
|
|
@@ -3057,9 +3085,11 @@ class OT3API(
|
|
|
3057
3085
|
async def aspirate_while_tracking(
|
|
3058
3086
|
self,
|
|
3059
3087
|
mount: Union[top_types.Mount, OT3Mount],
|
|
3060
|
-
|
|
3088
|
+
end_point: top_types.Point,
|
|
3061
3089
|
volume: float,
|
|
3062
|
-
|
|
3090
|
+
rate: float = 1.0,
|
|
3091
|
+
movement_delay: Optional[float] = None,
|
|
3092
|
+
end_critical_point: Optional[CriticalPoint] = None,
|
|
3063
3093
|
) -> None:
|
|
3064
3094
|
"""
|
|
3065
3095
|
Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
|
|
@@ -3067,19 +3097,35 @@ class OT3API(
|
|
|
3067
3097
|
:param mount: A robot mount that the instrument is on.
|
|
3068
3098
|
:param z_distance: The distance the z axis will move during apsiration.
|
|
3069
3099
|
:param volume: The volume of liquid to be aspirated.
|
|
3070
|
-
:param
|
|
3100
|
+
:param rate: The rate multiplier to aspirate with.
|
|
3101
|
+
:param movement_delay: Time to wait after the pipette starts aspirating before x/y/z movement.
|
|
3071
3102
|
"""
|
|
3072
3103
|
realmount = OT3Mount.from_mount(mount)
|
|
3073
3104
|
aspirate_spec = self._pipette_handler.plan_check_aspirate(
|
|
3074
|
-
realmount, volume,
|
|
3105
|
+
realmount, volume, rate
|
|
3075
3106
|
)
|
|
3076
3107
|
if not aspirate_spec:
|
|
3077
3108
|
return
|
|
3109
|
+
end_position = target_position_from_absolute(
|
|
3110
|
+
realmount,
|
|
3111
|
+
end_point,
|
|
3112
|
+
partial(self.critical_point_for, cp_override=end_critical_point),
|
|
3113
|
+
top_types.Point(*self._config.left_mount_offset),
|
|
3114
|
+
top_types.Point(*self._config.right_mount_offset),
|
|
3115
|
+
top_types.Point(*self._config.gripper_mount_offset),
|
|
3116
|
+
)
|
|
3117
|
+
|
|
3078
3118
|
target_pos = target_positions_from_plunger_tracking(
|
|
3079
3119
|
realmount,
|
|
3080
3120
|
aspirate_spec.plunger_distance,
|
|
3081
|
-
|
|
3082
|
-
|
|
3121
|
+
end_position,
|
|
3122
|
+
)
|
|
3123
|
+
|
|
3124
|
+
delay: Optional[Tuple[List[Axis], float]] = None
|
|
3125
|
+
if movement_delay is not None:
|
|
3126
|
+
delay = ([Axis.X, Axis.Y, Axis.Z_L, Axis.Z_R], movement_delay)
|
|
3127
|
+
self._log.info(
|
|
3128
|
+
f"aspirate_while_tracking: end at {end_point} {end_critical_point}, machine pos {end_position}, with plunger {target_pos}, delay {delay}"
|
|
3083
3129
|
)
|
|
3084
3130
|
try:
|
|
3085
3131
|
await self._backend.set_active_current(
|
|
@@ -3093,7 +3139,13 @@ class OT3API(
|
|
|
3093
3139
|
target_pos,
|
|
3094
3140
|
speed=aspirate_spec.speed,
|
|
3095
3141
|
home_flagged_axes=False,
|
|
3142
|
+
delay=delay,
|
|
3096
3143
|
)
|
|
3144
|
+
except PipetteOverpressureError:
|
|
3145
|
+
self._log.exception("Aspirate failed with overpressure")
|
|
3146
|
+
# refresh positions during an over pressure here so we know where the gantry stopped.
|
|
3147
|
+
await self.refresh_positions()
|
|
3148
|
+
raise
|
|
3097
3149
|
except Exception:
|
|
3098
3150
|
self._log.exception("Aspirate failed")
|
|
3099
3151
|
aspirate_spec.instr.set_current_volume(0)
|
|
@@ -3104,11 +3156,13 @@ class OT3API(
|
|
|
3104
3156
|
async def dispense_while_tracking(
|
|
3105
3157
|
self,
|
|
3106
3158
|
mount: Union[top_types.Mount, OT3Mount],
|
|
3107
|
-
|
|
3159
|
+
end_point: top_types.Point,
|
|
3108
3160
|
volume: float,
|
|
3109
3161
|
push_out: Optional[float],
|
|
3110
|
-
|
|
3162
|
+
rate: float = 1.0,
|
|
3111
3163
|
is_full_dispense: bool = False,
|
|
3164
|
+
movement_delay: Optional[float] = None,
|
|
3165
|
+
end_critical_point: Optional[CriticalPoint] = None,
|
|
3112
3166
|
) -> None:
|
|
3113
3167
|
"""
|
|
3114
3168
|
Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
|
|
@@ -3116,21 +3170,36 @@ class OT3API(
|
|
|
3116
3170
|
:param mount: A robot mount that the instrument is on.
|
|
3117
3171
|
:param z_distance: The distance the z axis will move during dispensing.
|
|
3118
3172
|
:param volume: The volume of liquid to be dispensed.
|
|
3119
|
-
:param
|
|
3173
|
+
:param rate: The rate multiplier to dispense with.
|
|
3174
|
+
:param movement_delay: Time to wait after the pipette starts dispensing before x/y/z movement.
|
|
3120
3175
|
"""
|
|
3121
3176
|
realmount = OT3Mount.from_mount(mount)
|
|
3122
3177
|
dispense_spec = self._pipette_handler.plan_check_dispense(
|
|
3123
|
-
realmount, volume,
|
|
3178
|
+
realmount, volume, rate, push_out, is_full_dispense
|
|
3124
3179
|
)
|
|
3125
3180
|
if not dispense_spec:
|
|
3126
3181
|
return
|
|
3182
|
+
end_position = target_position_from_absolute(
|
|
3183
|
+
realmount,
|
|
3184
|
+
end_point,
|
|
3185
|
+
partial(self.critical_point_for, cp_override=end_critical_point),
|
|
3186
|
+
top_types.Point(*self._config.left_mount_offset),
|
|
3187
|
+
top_types.Point(*self._config.right_mount_offset),
|
|
3188
|
+
top_types.Point(*self._config.gripper_mount_offset),
|
|
3189
|
+
)
|
|
3190
|
+
|
|
3127
3191
|
target_pos = target_positions_from_plunger_tracking(
|
|
3128
3192
|
realmount,
|
|
3129
3193
|
dispense_spec.plunger_distance,
|
|
3130
|
-
|
|
3131
|
-
self._current_position,
|
|
3194
|
+
end_position,
|
|
3132
3195
|
)
|
|
3133
3196
|
|
|
3197
|
+
delay: Optional[Tuple[List[Axis], float]] = None
|
|
3198
|
+
if movement_delay is not None:
|
|
3199
|
+
delay = ([Axis.X, Axis.Y, Axis.Z_L, Axis.Z_R], movement_delay)
|
|
3200
|
+
self._log.info(
|
|
3201
|
+
f"dispense_while_tracking: end at {end_point} {end_critical_point}, machine pos {end_position}, with plunger {target_pos}, delay {delay}"
|
|
3202
|
+
)
|
|
3134
3203
|
try:
|
|
3135
3204
|
await self._backend.set_active_current(
|
|
3136
3205
|
{dispense_spec.axis: dispense_spec.current}
|
|
@@ -3143,7 +3212,13 @@ class OT3API(
|
|
|
3143
3212
|
target_pos,
|
|
3144
3213
|
speed=dispense_spec.speed,
|
|
3145
3214
|
home_flagged_axes=False,
|
|
3215
|
+
delay=delay,
|
|
3146
3216
|
)
|
|
3217
|
+
except PipetteOverpressureError:
|
|
3218
|
+
self._log.exception("Aspirate failed with overpressure")
|
|
3219
|
+
# refresh positions during an over pressure here so we know where the gantry stopped.
|
|
3220
|
+
await self.refresh_positions()
|
|
3221
|
+
raise
|
|
3147
3222
|
except Exception:
|
|
3148
3223
|
self._log.exception("dispense failed")
|
|
3149
3224
|
dispense_spec.instr.set_current_volume(0)
|
|
@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
from typing import AsyncGenerator, List, Optional
|
|
6
6
|
from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnectedError
|
|
7
7
|
from opentrons_shared_data.errors.exceptions import ModuleCommunicationError
|
|
8
|
+
from opentrons.drivers.asyncio.communication.errors import SerialException
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
log = logging.getLogger(__name__)
|
|
@@ -88,6 +89,18 @@ class Poller:
|
|
|
88
89
|
except asyncio.InvalidStateError:
|
|
89
90
|
log.warning("Poller waiter was already cancelled")
|
|
90
91
|
|
|
92
|
+
def _error_callback(self, exc: Exception) -> None:
|
|
93
|
+
try:
|
|
94
|
+
self._reader.on_error(exc)
|
|
95
|
+
except Exception:
|
|
96
|
+
log.exception("Exception in reader callback")
|
|
97
|
+
|
|
98
|
+
def _complete_all(
|
|
99
|
+
self, exc: Exception | None, previous: List["asyncio.Future[None]"]
|
|
100
|
+
) -> None:
|
|
101
|
+
for waiter in previous:
|
|
102
|
+
Poller._set_waiter_complete(waiter, exc)
|
|
103
|
+
|
|
91
104
|
async def _poll_once(self) -> None:
|
|
92
105
|
"""Trigger a single read, notifying listeners of success or error."""
|
|
93
106
|
previous_waiters = self._poll_waiters
|
|
@@ -99,14 +112,15 @@ class Poller:
|
|
|
99
112
|
except asyncio.CancelledError:
|
|
100
113
|
raise
|
|
101
114
|
except AbsorbanceReaderDisconnectedError as e:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
self._error_callback(e)
|
|
116
|
+
self._complete_all(e, previous_waiters)
|
|
117
|
+
except SerialException as se:
|
|
118
|
+
log.error(f"Polling gcode error: {se}")
|
|
119
|
+
self._error_callback(se)
|
|
120
|
+
self._complete_all(se, previous_waiters)
|
|
105
121
|
except Exception as e:
|
|
106
122
|
log.exception("Polling exception")
|
|
107
|
-
self.
|
|
108
|
-
|
|
109
|
-
Poller._set_waiter_complete(waiter, e)
|
|
123
|
+
self._error_callback(e)
|
|
124
|
+
self._complete_all(e, previous_waiters)
|
|
110
125
|
else:
|
|
111
|
-
|
|
112
|
-
Poller._set_waiter_complete(waiter)
|
|
126
|
+
self._complete_all(None, previous_waiters)
|
|
@@ -125,17 +125,21 @@ class LiquidHandler(
|
|
|
125
125
|
async def aspirate_while_tracking(
|
|
126
126
|
self,
|
|
127
127
|
mount: MountArgType,
|
|
128
|
-
|
|
128
|
+
end_point: Point,
|
|
129
129
|
volume: float,
|
|
130
130
|
flow_rate: float = 1.0,
|
|
131
|
+
movement_delay: Optional[float] = None,
|
|
132
|
+
end_critical_point: Optional[CriticalPoint] = None,
|
|
131
133
|
) -> None:
|
|
132
134
|
"""
|
|
133
135
|
Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
|
|
134
136
|
|
|
135
137
|
:param mount: A robot mount that the instrument is on.
|
|
136
|
-
:param
|
|
138
|
+
:param end_point: The deck coordinate point to move the tip to during the aspirate.
|
|
137
139
|
:param volume: The volume of liquid to be aspirated.
|
|
138
140
|
:param flow_rate: The flow rate to aspirate with.
|
|
141
|
+
:param movement_delay: Time to wait after the pipette starts aspirating before x/y/z movement.
|
|
142
|
+
:param end_critical_point: The critical point for the end_point position.
|
|
139
143
|
"""
|
|
140
144
|
...
|
|
141
145
|
|
|
@@ -164,19 +168,23 @@ class LiquidHandler(
|
|
|
164
168
|
async def dispense_while_tracking(
|
|
165
169
|
self,
|
|
166
170
|
mount: MountArgType,
|
|
167
|
-
|
|
171
|
+
end_point: Point,
|
|
168
172
|
volume: float,
|
|
169
173
|
push_out: Optional[float],
|
|
170
174
|
flow_rate: float = 1.0,
|
|
171
175
|
is_full_dispense: bool = False,
|
|
176
|
+
movement_delay: Optional[float] = None,
|
|
177
|
+
end_critical_point: Optional[CriticalPoint] = None,
|
|
172
178
|
) -> None:
|
|
173
179
|
"""
|
|
174
180
|
Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
|
|
175
181
|
|
|
176
182
|
:param mount: A robot mount that the instrument is on.
|
|
177
|
-
:param
|
|
183
|
+
:param end_point: The deck coordinate point to move the tip to during the dispense.
|
|
178
184
|
:param volume: The volume of liquid to be dispensed.
|
|
179
185
|
:param flow_rate: The flow rate to dispense with.
|
|
186
|
+
:param movement_delay: Time to wait after the pipette starts dispensing before x/y/z movement.
|
|
187
|
+
:param end_critical_point: The critical point for the end_point position.
|
|
180
188
|
"""
|
|
181
189
|
...
|
|
182
190
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Module Firmware update script."""
|
|
2
|
+
|
|
2
3
|
import argparse
|
|
3
4
|
import asyncio
|
|
4
5
|
from glob import glob
|
|
@@ -14,6 +15,7 @@ from opentrons.hardware_control import modules
|
|
|
14
15
|
from opentrons.hardware_control.modules.mod_abc import AbstractModule
|
|
15
16
|
from opentrons.hardware_control.modules.update import update_firmware
|
|
16
17
|
from opentrons.hardware_control.types import BoardRevision
|
|
18
|
+
from opentrons.hardware_control.execution_manager import ExecutionManager
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
# Constants for checking if module is back online
|
|
@@ -84,6 +86,9 @@ async def build_module(
|
|
|
84
86
|
port=port,
|
|
85
87
|
usb_port=mod.usb_port,
|
|
86
88
|
type=modules.MODULE_TYPE_BY_NAME[mod.name],
|
|
89
|
+
execution_manager=ExecutionManager(),
|
|
90
|
+
disconnected_callback=lambda *args: None,
|
|
91
|
+
error_callback=lambda *args: None,
|
|
87
92
|
simulating=False,
|
|
88
93
|
hw_control_loop=loop,
|
|
89
94
|
)
|
|
@@ -2,12 +2,26 @@ from asyncio import Queue
|
|
|
2
2
|
import enum
|
|
3
3
|
import logging
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import (
|
|
6
|
+
cast,
|
|
7
|
+
Tuple,
|
|
8
|
+
Union,
|
|
9
|
+
List,
|
|
10
|
+
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
TypeVar,
|
|
13
|
+
Type,
|
|
14
|
+
TYPE_CHECKING,
|
|
15
|
+
)
|
|
6
16
|
from typing_extensions import Literal
|
|
7
17
|
from opentrons import types as top_types
|
|
8
18
|
from opentrons_shared_data.pipette.types import PipetteChannelType
|
|
19
|
+
from opentrons_shared_data.errors.exceptions import EnumeratedError
|
|
9
20
|
from opentrons.config import feature_flags
|
|
10
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from .modules.types import ModuleModel
|
|
24
|
+
|
|
11
25
|
MODULE_LOG = logging.getLogger(__name__)
|
|
12
26
|
|
|
13
27
|
|
|
@@ -384,6 +398,8 @@ class HardwareEventType(enum.Enum):
|
|
|
384
398
|
DOOR_SWITCH_CHANGE = enum.auto()
|
|
385
399
|
ERROR_MESSAGE = enum.auto()
|
|
386
400
|
ESTOP_CHANGE = enum.auto()
|
|
401
|
+
ASYNCHRONOUS_MODULE_ERROR = enum.auto()
|
|
402
|
+
MODULE_DISCONNECTED = enum.auto()
|
|
387
403
|
|
|
388
404
|
|
|
389
405
|
@dataclass
|
|
@@ -428,10 +444,35 @@ class ErrorMessageNotification:
|
|
|
428
444
|
event: Literal[HardwareEventType.ERROR_MESSAGE] = HardwareEventType.ERROR_MESSAGE
|
|
429
445
|
|
|
430
446
|
|
|
447
|
+
@dataclass(frozen=True)
|
|
448
|
+
class AsynchronousModuleErrorNotification:
|
|
449
|
+
exception: EnumeratedError
|
|
450
|
+
module_serial: str | None
|
|
451
|
+
module_model: "ModuleModel"
|
|
452
|
+
port: str
|
|
453
|
+
event: Literal[
|
|
454
|
+
HardwareEventType.ASYNCHRONOUS_MODULE_ERROR
|
|
455
|
+
] = HardwareEventType.ASYNCHRONOUS_MODULE_ERROR
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@dataclass(frozen=True)
|
|
459
|
+
class ModuleDisconnectedNotification:
|
|
460
|
+
module_serial: str | None
|
|
461
|
+
module_model: "ModuleModel"
|
|
462
|
+
port: str
|
|
463
|
+
event: Literal[
|
|
464
|
+
HardwareEventType.MODULE_DISCONNECTED
|
|
465
|
+
] = HardwareEventType.MODULE_DISCONNECTED
|
|
466
|
+
|
|
467
|
+
|
|
431
468
|
# new event types get new dataclasses
|
|
432
469
|
# when we add more event types we add them here
|
|
433
470
|
HardwareEvent = Union[
|
|
434
|
-
DoorStateNotification,
|
|
471
|
+
DoorStateNotification,
|
|
472
|
+
ErrorMessageNotification,
|
|
473
|
+
EstopStateNotification,
|
|
474
|
+
AsynchronousModuleErrorNotification,
|
|
475
|
+
ModuleDisconnectedNotification,
|
|
435
476
|
]
|
|
436
477
|
|
|
437
478
|
HardwareEventHandler = Callable[[HardwareEvent], None]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import TYPE_CHECKING, List, Sequence, Union, overload
|
|
2
|
+
from typing import TYPE_CHECKING, List, Sequence, Union, overload, Optional
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
from .helpers import (
|
|
@@ -31,10 +31,21 @@ def aspirate(
|
|
|
31
31
|
location: Location,
|
|
32
32
|
flow_rate: float,
|
|
33
33
|
rate: float,
|
|
34
|
+
end_location: Optional[Location],
|
|
34
35
|
) -> command_types.AspirateCommand:
|
|
35
36
|
location_text = stringify_location(location)
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
end_location_text = (
|
|
38
|
+
f" while moving to {stringify_location(end_location)}"
|
|
39
|
+
if end_location is not None
|
|
40
|
+
else ""
|
|
41
|
+
)
|
|
42
|
+
template = "Aspirating {volume} uL from {location} at {flow} uL/sec{end}"
|
|
43
|
+
text = template.format(
|
|
44
|
+
volume=float(volume),
|
|
45
|
+
location=location_text,
|
|
46
|
+
flow=flow_rate,
|
|
47
|
+
end=end_location_text,
|
|
48
|
+
)
|
|
38
49
|
|
|
39
50
|
return {
|
|
40
51
|
"name": command_types.ASPIRATE,
|
|
@@ -44,6 +55,7 @@ def aspirate(
|
|
|
44
55
|
"location": location,
|
|
45
56
|
"rate": rate,
|
|
46
57
|
"text": text,
|
|
58
|
+
"end_location": end_location,
|
|
47
59
|
},
|
|
48
60
|
}
|
|
49
61
|
|
|
@@ -54,10 +66,21 @@ def dispense(
|
|
|
54
66
|
location: Location,
|
|
55
67
|
flow_rate: float,
|
|
56
68
|
rate: float,
|
|
69
|
+
end_location: Optional[Location],
|
|
57
70
|
) -> command_types.DispenseCommand:
|
|
58
71
|
location_text = stringify_location(location)
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
end_location_text = (
|
|
73
|
+
f" while moving to {stringify_location(end_location)}"
|
|
74
|
+
if end_location is not None
|
|
75
|
+
else ""
|
|
76
|
+
)
|
|
77
|
+
template = "Dispensing {volume} uL into {location} at {flow} uL/sec{end}"
|
|
78
|
+
text = template.format(
|
|
79
|
+
volume=float(volume),
|
|
80
|
+
location=location_text,
|
|
81
|
+
flow=flow_rate,
|
|
82
|
+
end=end_location_text,
|
|
83
|
+
)
|
|
61
84
|
|
|
62
85
|
return {
|
|
63
86
|
"name": command_types.DISPENSE,
|
|
@@ -67,6 +90,7 @@ def dispense(
|
|
|
67
90
|
"location": location,
|
|
68
91
|
"rate": rate,
|
|
69
92
|
"text": text,
|
|
93
|
+
"end_location": end_location,
|
|
70
94
|
},
|
|
71
95
|
}
|
|
72
96
|
|
|
@@ -208,6 +232,35 @@ def mix(
|
|
|
208
232
|
}
|
|
209
233
|
|
|
210
234
|
|
|
235
|
+
def dynamic_mix(
|
|
236
|
+
instrument: InstrumentContext,
|
|
237
|
+
repetitions: int,
|
|
238
|
+
volume: float,
|
|
239
|
+
aspirate_start_location: Location,
|
|
240
|
+
aspirate_end_location: Union[Location, None],
|
|
241
|
+
dispense_start_location: Location,
|
|
242
|
+
dispense_end_location: Union[Location, None],
|
|
243
|
+
movement_delay: float,
|
|
244
|
+
) -> command_types.DynamicMixCommand:
|
|
245
|
+
text = "Dynamically mixing {repetitions} times with a volume of {volume} ul".format(
|
|
246
|
+
repetitions=repetitions, volume=float(volume)
|
|
247
|
+
)
|
|
248
|
+
return {
|
|
249
|
+
"name": command_types.MIX,
|
|
250
|
+
"payload": {
|
|
251
|
+
"instrument": instrument,
|
|
252
|
+
"aspirate_start_location": aspirate_start_location,
|
|
253
|
+
"aspirate_end_location": aspirate_end_location,
|
|
254
|
+
"dispense_start_location": dispense_start_location,
|
|
255
|
+
"dispense_end_location": dispense_end_location,
|
|
256
|
+
"volume": volume,
|
|
257
|
+
"repetitions": repetitions,
|
|
258
|
+
"text": text,
|
|
259
|
+
"movement_delay": movement_delay,
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
211
264
|
def blow_out(
|
|
212
265
|
instrument: InstrumentContext, location: Location
|
|
213
266
|
) -> command_types.BlowOutCommand:
|
|
@@ -89,6 +89,24 @@ def thermocycler_set_block_temp(
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
def thermocycler_start_set_block_temp(
|
|
93
|
+
temperature: float,
|
|
94
|
+
) -> command_types.ThermocyclerStartSetBlockTempCommand:
|
|
95
|
+
temp = round(float(temperature), utils.TC_GCODE_ROUNDING_PRECISION)
|
|
96
|
+
text = f"Starting to set Thermocycler well block temperature to {temp} °C"
|
|
97
|
+
# TODO: BC 2019-09-05 this time resolving logic is partially duplicated
|
|
98
|
+
# in the thermocycler api class definition, with this command logger
|
|
99
|
+
# implementation, there isn't a great way to avoid this, but it should
|
|
100
|
+
# be consolidated as soon as an alternative to the publisher is settled on.
|
|
101
|
+
return {
|
|
102
|
+
"name": command_types.THERMOCYCLER_START_SET_BLOCK_TEMP,
|
|
103
|
+
"payload": {
|
|
104
|
+
"temperature": temperature,
|
|
105
|
+
"text": text,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
92
110
|
def thermocycler_execute_profile(
|
|
93
111
|
steps: List[ThermocyclerStep], repetitions: int
|
|
94
112
|
) -> command_types.ThermocyclerExecuteProfileCommand:
|
|
@@ -102,6 +120,19 @@ def thermocycler_execute_profile(
|
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
|
|
123
|
+
def thermocycler_start_execute_profile(
|
|
124
|
+
steps: List[ThermocyclerStep], repetitions: int
|
|
125
|
+
) -> command_types.ThermocyclerStartExecuteProfileCommand:
|
|
126
|
+
text = (
|
|
127
|
+
f"In the background, thermocycler starting to run {repetitions} repetitions "
|
|
128
|
+
f" of cycle composed of the following steps: {steps}"
|
|
129
|
+
)
|
|
130
|
+
return {
|
|
131
|
+
"name": command_types.THERMOCYCLER_START_EXECUTE_PROFILE,
|
|
132
|
+
"payload": {"text": text, "steps": steps},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
105
136
|
def thermocycler_wait_for_hold() -> command_types.ThermocyclerWaitForHoldCommand:
|
|
106
137
|
text = "Waiting for hold time duration"
|
|
107
138
|
return {"name": command_types.THERMOCYCLER_WAIT_FOR_HOLD, "payload": {"text": text}}
|
|
@@ -120,6 +151,17 @@ def thermocycler_set_lid_temperature(
|
|
|
120
151
|
return {"name": command_types.THERMOCYCLER_SET_LID_TEMP, "payload": {"text": text}}
|
|
121
152
|
|
|
122
153
|
|
|
154
|
+
def thermocycler_start_set_lid_temperature(
|
|
155
|
+
temperature: float,
|
|
156
|
+
) -> command_types.ThermocyclerStartSetLidTempCommand:
|
|
157
|
+
temp = round(float(temperature), utils.TC_GCODE_ROUNDING_PRECISION)
|
|
158
|
+
text = f"Starting to set Thermocycler lid temperature to {temp} °C"
|
|
159
|
+
return {
|
|
160
|
+
"name": command_types.THERMOCYCLER_START_SET_LID_TEMP,
|
|
161
|
+
"payload": {"text": text},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
123
165
|
def thermocycler_deactivate_lid() -> command_types.ThermocyclerDeactivateLidCommand:
|
|
124
166
|
text = "Deactivating Thermocycler lid heating"
|
|
125
167
|
return {
|
|
@@ -183,6 +225,16 @@ def heater_shaker_set_and_wait_for_shake_speed(
|
|
|
183
225
|
}
|
|
184
226
|
|
|
185
227
|
|
|
228
|
+
def heater_shaker_set_shake_speed(
|
|
229
|
+
rpm: int,
|
|
230
|
+
) -> command_types.HeaterShakerSetShakeSpeedCommand:
|
|
231
|
+
text = f"Setting Heater-Shaker to Shake at {rpm} RPM"
|
|
232
|
+
return {
|
|
233
|
+
"name": command_types.HEATER_SHAKER_SET_SHAKE_SPEED,
|
|
234
|
+
"payload": {"text": text},
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
186
238
|
def heater_shaker_open_labware_latch() -> command_types.HeaterShakerOpenLabwareLatchCommand:
|
|
187
239
|
text = "Unlatching labware on Heater-Shaker"
|
|
188
240
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from datetime import timedelta
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
3
|
from . import types as command_types
|
|
4
|
+
from opentrons.protocol_api.tasks import Task
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def comment(msg: str) -> command_types.CommentCommand:
|
|
@@ -52,3 +53,54 @@ def move_labware(text: str) -> command_types.MoveLabwareCommand:
|
|
|
52
53
|
"name": command_types.MOVE_LABWARE,
|
|
53
54
|
"payload": {"text": text},
|
|
54
55
|
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def capture_image(
|
|
59
|
+
resolution: Optional[Tuple[int, int]],
|
|
60
|
+
zoom: Optional[float],
|
|
61
|
+
contrast: Optional[float],
|
|
62
|
+
brightness: Optional[float],
|
|
63
|
+
saturation: Optional[float],
|
|
64
|
+
) -> command_types.CaptureImageCommand:
|
|
65
|
+
text = "Capturing an image"
|
|
66
|
+
if resolution:
|
|
67
|
+
text += f" with resolution {resolution[0]}x{resolution[1]}"
|
|
68
|
+
if zoom:
|
|
69
|
+
text += f" zoom of {zoom}X"
|
|
70
|
+
if contrast:
|
|
71
|
+
text += f" contrast of {contrast}%"
|
|
72
|
+
if brightness:
|
|
73
|
+
text += f" brightness of {brightness}%"
|
|
74
|
+
if saturation:
|
|
75
|
+
text += f" saturation of {saturation}%"
|
|
76
|
+
text += "."
|
|
77
|
+
return {
|
|
78
|
+
"name": command_types.CAPTURE_IMAGE,
|
|
79
|
+
"payload": {
|
|
80
|
+
"text": text,
|
|
81
|
+
"resolution": resolution,
|
|
82
|
+
"zoom": zoom,
|
|
83
|
+
"contrast": contrast,
|
|
84
|
+
"brightness": brightness,
|
|
85
|
+
"saturation": saturation,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def wait_for_tasks(tasks: list[Task]) -> command_types.WaitForTasksCommand:
|
|
91
|
+
task_ids = [task.created_at.strftime("%Y-%m-%d %H:%M:%S") for task in tasks]
|
|
92
|
+
msg = f"Waiting for tasks that started at: {task_ids}."
|
|
93
|
+
return {
|
|
94
|
+
"name": command_types.WAIT_FOR_TASKS,
|
|
95
|
+
"payload": {"text": msg},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def create_timer(seconds: float) -> command_types.CreateTimerCommand:
|
|
100
|
+
return {
|
|
101
|
+
"name": command_types.CREATE_TIMER,
|
|
102
|
+
"payload": {
|
|
103
|
+
"text": f"Creating background timer for {seconds} seconds.",
|
|
104
|
+
"time": seconds,
|
|
105
|
+
},
|
|
106
|
+
}
|