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
opentrons/simulate.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
This module has functions that provide a console entrypoint for simulating
|
|
4
4
|
a protocol from the command line.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import argparse
|
|
7
8
|
import asyncio
|
|
8
9
|
import atexit
|
|
@@ -24,6 +25,7 @@ from typing import (
|
|
|
24
25
|
BinaryIO,
|
|
25
26
|
Optional,
|
|
26
27
|
Union,
|
|
28
|
+
Iterator,
|
|
27
29
|
)
|
|
28
30
|
from typing_extensions import Literal
|
|
29
31
|
|
|
@@ -48,6 +50,7 @@ from opentrons.protocol_engine.create_protocol_engine import (
|
|
|
48
50
|
from opentrons.protocol_engine import error_recovery_policy
|
|
49
51
|
from opentrons.protocol_engine.state.config import Config
|
|
50
52
|
from opentrons.protocol_engine.types import DeckType, EngineStatus, PostRunHardwareState
|
|
53
|
+
from opentrons.protocol_engine.resources.camera_provider import CameraProvider
|
|
51
54
|
from opentrons.protocol_reader.protocol_source import ProtocolSource
|
|
52
55
|
from opentrons.protocol_runner.protocol_runner import create_protocol_runner, LiveRunner
|
|
53
56
|
from opentrons.protocol_runner import RunOrchestrator
|
|
@@ -81,6 +84,7 @@ if TYPE_CHECKING:
|
|
|
81
84
|
from opentrons_shared_data.labware.types import (
|
|
82
85
|
LabwareDefinition as LabwareDefinitionDict,
|
|
83
86
|
)
|
|
87
|
+
from opentrons.protocol_engine import ProtocolEngine
|
|
84
88
|
|
|
85
89
|
|
|
86
90
|
# See Jira RCORE-535.
|
|
@@ -309,6 +313,7 @@ def get_protocol_api(
|
|
|
309
313
|
bundled_labware=bundled_labware,
|
|
310
314
|
bundled_data=bundled_data,
|
|
311
315
|
extra_labware=extra_labware,
|
|
316
|
+
clean_up_hardware=(hardware_simulator is None),
|
|
312
317
|
)
|
|
313
318
|
else:
|
|
314
319
|
if bundled_labware is not None:
|
|
@@ -326,6 +331,7 @@ def get_protocol_api(
|
|
|
326
331
|
bundled_data=bundled_data,
|
|
327
332
|
extra_labware=extra_labware,
|
|
328
333
|
use_pe_virtual_hardware=use_virtual_hardware,
|
|
334
|
+
clean_up_hardware=(hardware_simulator is None),
|
|
329
335
|
)
|
|
330
336
|
|
|
331
337
|
# Intentional difference from execute.get_protocol_api():
|
|
@@ -781,13 +787,15 @@ def _create_live_context_non_pe(
|
|
|
781
787
|
extra_labware: Optional[Dict[str, "LabwareDefinitionDict"]],
|
|
782
788
|
bundled_labware: Optional[Dict[str, "LabwareDefinitionDict"]],
|
|
783
789
|
bundled_data: Optional[Dict[str, bytes]],
|
|
790
|
+
clean_up_hardware: bool,
|
|
784
791
|
) -> ProtocolContext:
|
|
785
792
|
"""Return a live ProtocolContext.
|
|
786
793
|
|
|
787
794
|
This controls the robot through the older infrastructure, instead of through Protocol Engine.
|
|
788
795
|
"""
|
|
789
796
|
assert api_version < ENGINE_CORE_API_VERSION
|
|
790
|
-
|
|
797
|
+
|
|
798
|
+
ctx = protocol_api.create_protocol_context(
|
|
791
799
|
api_version=api_version,
|
|
792
800
|
deck_type=deck_type,
|
|
793
801
|
hardware_api=hardware_api,
|
|
@@ -795,6 +803,17 @@ def _create_live_context_non_pe(
|
|
|
795
803
|
bundled_data=bundled_data,
|
|
796
804
|
extra_labware=extra_labware,
|
|
797
805
|
)
|
|
806
|
+
# Hack: we need to hook the protocol context cleanup in a way that isn't safe to put
|
|
807
|
+
# in the context generally, so we can do it like this and feel sad
|
|
808
|
+
original_cleanup = ctx.cleanup
|
|
809
|
+
|
|
810
|
+
def _cleanup_hook() -> None:
|
|
811
|
+
if clean_up_hardware:
|
|
812
|
+
ctx._hw_manager.hardware.clean_up()
|
|
813
|
+
original_cleanup()
|
|
814
|
+
|
|
815
|
+
ctx.cleanup = _cleanup_hook # type: ignore[method-assign]
|
|
816
|
+
return ctx
|
|
798
817
|
|
|
799
818
|
|
|
800
819
|
def _create_live_context_pe(
|
|
@@ -805,26 +824,41 @@ def _create_live_context_pe(
|
|
|
805
824
|
extra_labware: Dict[str, "LabwareDefinitionDict"],
|
|
806
825
|
bundled_data: Optional[Dict[str, bytes]],
|
|
807
826
|
use_pe_virtual_hardware: bool = True,
|
|
827
|
+
clean_up_hardware: bool = True,
|
|
808
828
|
) -> ProtocolContext:
|
|
809
829
|
"""Return a live ProtocolContext that controls the robot through ProtocolEngine."""
|
|
810
830
|
assert api_version >= ENGINE_CORE_API_VERSION
|
|
811
831
|
hardware_api_wrapped = hardware_api.wrapped()
|
|
812
832
|
global _LIVE_PROTOCOL_ENGINE_CONTEXTS
|
|
833
|
+
|
|
834
|
+
@contextmanager
|
|
835
|
+
def _cleanup_hardware_with_engine() -> (
|
|
836
|
+
Iterator[tuple["ProtocolEngine", asyncio.AbstractEventLoop]]
|
|
837
|
+
):
|
|
838
|
+
|
|
839
|
+
try:
|
|
840
|
+
with create_protocol_engine_in_thread(
|
|
841
|
+
hardware_api=hardware_api_wrapped,
|
|
842
|
+
config=_get_protocol_engine_config(
|
|
843
|
+
robot_type, use_pe_virtual_hardware=use_pe_virtual_hardware
|
|
844
|
+
),
|
|
845
|
+
deck_configuration=None,
|
|
846
|
+
file_provider=None,
|
|
847
|
+
error_recovery_policy=error_recovery_policy.never_recover,
|
|
848
|
+
drop_tips_after_run=False,
|
|
849
|
+
post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE,
|
|
850
|
+
load_fixed_trash=should_load_fixed_trash_labware_for_python_protocol(
|
|
851
|
+
api_version
|
|
852
|
+
),
|
|
853
|
+
camera_provider=CameraProvider(),
|
|
854
|
+
) as (engine, loop):
|
|
855
|
+
yield engine, loop
|
|
856
|
+
finally:
|
|
857
|
+
if clean_up_hardware:
|
|
858
|
+
hardware_api.clean_up()
|
|
859
|
+
|
|
813
860
|
pe, loop = _LIVE_PROTOCOL_ENGINE_CONTEXTS.enter_context(
|
|
814
|
-
|
|
815
|
-
hardware_api=hardware_api_wrapped,
|
|
816
|
-
config=_get_protocol_engine_config(
|
|
817
|
-
robot_type, use_pe_virtual_hardware=use_pe_virtual_hardware
|
|
818
|
-
),
|
|
819
|
-
deck_configuration=None,
|
|
820
|
-
file_provider=None,
|
|
821
|
-
error_recovery_policy=error_recovery_policy.never_recover,
|
|
822
|
-
drop_tips_after_run=False,
|
|
823
|
-
post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE,
|
|
824
|
-
load_fixed_trash=should_load_fixed_trash_labware_for_python_protocol(
|
|
825
|
-
api_version
|
|
826
|
-
),
|
|
827
|
-
)
|
|
861
|
+
_cleanup_hardware_with_engine()
|
|
828
862
|
)
|
|
829
863
|
|
|
830
864
|
# `async def` so we can use loop.run_coroutine_threadsafe() to wait for its completion.
|
|
@@ -879,6 +913,7 @@ def _run_file_non_pe(
|
|
|
879
913
|
extra_labware=extra_labware,
|
|
880
914
|
bundled_labware=bundled_labware,
|
|
881
915
|
bundled_data=bundled_data,
|
|
916
|
+
clean_up_hardware=False,
|
|
882
917
|
)
|
|
883
918
|
|
|
884
919
|
scraper = _CommandScraper(logger=logger, level=level, broker=context.broker)
|
|
@@ -949,6 +984,7 @@ def _run_file_pe(
|
|
|
949
984
|
protocol_live_runner=LiveRunner(
|
|
950
985
|
protocol_engine=protocol_engine, hardware_api=hardware_api_wrapped
|
|
951
986
|
),
|
|
987
|
+
camera_provider=CameraProvider(),
|
|
952
988
|
)
|
|
953
989
|
|
|
954
990
|
# TODO(mm, 2024-08-06): This home is theoretically redundant with Protocol
|
opentrons/system/camera.py
CHANGED
|
@@ -1,10 +1,68 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from
|
|
4
|
+
import logging
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
from opentrons.config import ARCHITECTURE, SystemArchitecture, get_opentrons_path
|
|
6
9
|
from opentrons_shared_data.errors.exceptions import CommunicationError
|
|
7
10
|
from opentrons_shared_data.errors.codes import ErrorCodes
|
|
11
|
+
from opentrons.config import IS_ROBOT
|
|
12
|
+
from opentrons_shared_data.robot.types import RobotType, RobotTypeEnum
|
|
13
|
+
from opentrons.protocol_engine.resources.camera_provider import (
|
|
14
|
+
CameraProvider,
|
|
15
|
+
ImageParameters,
|
|
16
|
+
CameraError,
|
|
17
|
+
CameraSettings,
|
|
18
|
+
)
|
|
19
|
+
from opentrons.system import ffmpeg
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Default System Cameras
|
|
24
|
+
DEFAULT_SYSTEM_CAMERA = "/dev/ot_system_camera"
|
|
25
|
+
|
|
26
|
+
# Stream Globals
|
|
27
|
+
DEFAULT_CONF_FILE = (
|
|
28
|
+
"/lib/systemd/system/opentrons-live-stream/opentrons-live-stream.env"
|
|
29
|
+
)
|
|
30
|
+
STREAM_CONF_FILE_KEYS = [
|
|
31
|
+
"BOOT_ID",
|
|
32
|
+
"STATUS",
|
|
33
|
+
"SOURCE",
|
|
34
|
+
"RESOLUTION",
|
|
35
|
+
"FRAMERATE",
|
|
36
|
+
"BITRATE",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# Camera Parameter Globals
|
|
40
|
+
RESOLUTION_MIN = (320, 240)
|
|
41
|
+
RESOLUTION_MAX = (7680, 4320)
|
|
42
|
+
RESOLUTION_DEFAULT = (1920, 1080)
|
|
43
|
+
ZOOM_MIN = 1.0
|
|
44
|
+
ZOOM_MAX = 2.0
|
|
45
|
+
ZOOM_DEFAULT = 1.0
|
|
46
|
+
CONTRAST_MIN = 0.0
|
|
47
|
+
CONTRAST_MAX = 2.0
|
|
48
|
+
CONTRAST_DEFAULT = 1.0
|
|
49
|
+
BRIGHTNESS_MIN = -128
|
|
50
|
+
BRIGHTNESS_MAX = 128
|
|
51
|
+
BRIGHTNESS_DEFAULT = 0
|
|
52
|
+
SATURATION_MIN = 0.0
|
|
53
|
+
SATURATION_MAX = 2.0
|
|
54
|
+
SATURATION_DEFAULT = 1.0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StreamConfigurationKeys(str, Enum):
|
|
58
|
+
"""The Configuration Key Types."""
|
|
59
|
+
|
|
60
|
+
BOOT_ID = "BOOT_ID"
|
|
61
|
+
STATUS = "STATUS"
|
|
62
|
+
SOURCE = "SOURCE"
|
|
63
|
+
RESOLUTION = "RESOLUTION"
|
|
64
|
+
FRAMERATE = "FRAMERATE"
|
|
65
|
+
BITRATE = "BITRATE"
|
|
8
66
|
|
|
9
67
|
|
|
10
68
|
class CameraException(CommunicationError):
|
|
@@ -17,7 +75,7 @@ class CameraException(CommunicationError):
|
|
|
17
75
|
|
|
18
76
|
|
|
19
77
|
async def take_picture(filename: Path) -> None:
|
|
20
|
-
"""
|
|
78
|
+
"""Legacy method to take a picture and save it to filename
|
|
21
79
|
|
|
22
80
|
:param filename: Name of file to save picture to
|
|
23
81
|
:param loop: optional loop to use
|
|
@@ -30,7 +88,7 @@ async def take_picture(filename: Path) -> None:
|
|
|
30
88
|
pass
|
|
31
89
|
|
|
32
90
|
if ARCHITECTURE == SystemArchitecture.YOCTO:
|
|
33
|
-
cmd = f"v4l2-ctl --device /dev/
|
|
91
|
+
cmd = f"v4l2-ctl --device /dev/ot_system_camera --set-fmt-video=width=1280,height=720,pixelformat=MJPG --stream-mmap --stream-to={str(filename)} --stream-count=1"
|
|
34
92
|
elif ARCHITECTURE == SystemArchitecture.BUILDROOT:
|
|
35
93
|
cmd = f"ffmpeg -f video4linux2 -s 640x480 -i /dev/video0 -ss 0:0:1 -frames 1 {str(filename)}"
|
|
36
94
|
else: # HOST
|
|
@@ -49,3 +107,275 @@ async def take_picture(filename: Path) -> None:
|
|
|
49
107
|
raise CameraException("Failed to communicate with camera", res)
|
|
50
108
|
if not filename.exists():
|
|
51
109
|
raise CameraException("Failed to save image", "")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_stream_configuration_filepath() -> Path:
|
|
113
|
+
"""Return the file path to the Opentrons Live Stream Configuration file."""
|
|
114
|
+
filepath = get_opentrons_path("live_stream_environment_file")
|
|
115
|
+
if IS_ROBOT and not os.path.exists(filepath):
|
|
116
|
+
# If the dynamic configuration file doesn't exist make it using our defaults file
|
|
117
|
+
with open(DEFAULT_CONF_FILE, "r") as default_config:
|
|
118
|
+
content = default_config.read()
|
|
119
|
+
with open(filepath, "w") as new_config_file:
|
|
120
|
+
new_config_file.write(content)
|
|
121
|
+
return filepath
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def robot_supports_livestream(robot_type: RobotType) -> bool:
|
|
125
|
+
"""Validate whether or not robot supports live streaming service."""
|
|
126
|
+
robot = RobotTypeEnum.robot_literal_to_enum(robot_type)
|
|
127
|
+
if robot == RobotTypeEnum.OT2:
|
|
128
|
+
# If we are on an OT-2 we do not support live streams
|
|
129
|
+
return False
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def update_live_stream_status(
|
|
134
|
+
robot_type: RobotType,
|
|
135
|
+
stream_status: bool,
|
|
136
|
+
camera_provider: CameraProvider,
|
|
137
|
+
override_settings: Optional[CameraSettings] = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Update and handle a change in the Opentrons Live Stream status."""
|
|
140
|
+
if not IS_ROBOT or robot_supports_livestream(robot_type) is False:
|
|
141
|
+
# If we are not on a robot we simply no-op updating the stream
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
contents = load_stream_configuration_file_data()
|
|
145
|
+
if contents is None:
|
|
146
|
+
log.error("Opentrons Live Stream Configuration file cannot be updated.")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# Validate the stream status
|
|
150
|
+
if override_settings is not None:
|
|
151
|
+
camera_enable_settings = override_settings
|
|
152
|
+
else:
|
|
153
|
+
camera_enable_settings = await camera_provider.get_camera_settings()
|
|
154
|
+
status = "OFF"
|
|
155
|
+
if (
|
|
156
|
+
stream_status
|
|
157
|
+
and camera_enable_settings.cameraEnabled
|
|
158
|
+
and camera_enable_settings.liveStreamEnabled
|
|
159
|
+
):
|
|
160
|
+
# Check to see if the camera device is available
|
|
161
|
+
raw_device = str(contents["SOURCE"])[1:-1]
|
|
162
|
+
if not os.path.exists(raw_device):
|
|
163
|
+
log.error(
|
|
164
|
+
f"Opentrons Live Stream cannot sample the camera. No video device found with device path: {raw_device}"
|
|
165
|
+
)
|
|
166
|
+
# Enable the stream
|
|
167
|
+
status = "ON"
|
|
168
|
+
# Overwrite the contents
|
|
169
|
+
contents["BOOT_ID"] = get_boot_id()
|
|
170
|
+
contents["STATUS"] = status
|
|
171
|
+
write_stream_configuration_file_data(contents)
|
|
172
|
+
await restart_live_stream(robot_type)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def stop_live_stream(robot_type: RobotType) -> None:
|
|
176
|
+
"""Attempt to stop the Opentrons Live Stream service."""
|
|
177
|
+
if robot_supports_livestream(robot_type) is False:
|
|
178
|
+
# No-op on OT-2 since we don't have a live stream service there
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
command = ["systemctl", "stop", "opentrons-live-stream"]
|
|
182
|
+
subprocess = await asyncio.create_subprocess_exec(
|
|
183
|
+
*command,
|
|
184
|
+
stdout=asyncio.subprocess.PIPE,
|
|
185
|
+
stderr=asyncio.subprocess.PIPE,
|
|
186
|
+
)
|
|
187
|
+
stdout, stderr = await subprocess.communicate()
|
|
188
|
+
if subprocess.returncode == 0:
|
|
189
|
+
log.info("Stopped the opentrons-live-stream service.")
|
|
190
|
+
else:
|
|
191
|
+
log.error(
|
|
192
|
+
f"Failed to stop opentrons-live-stream, returncode:{ subprocess.returncode}, stdout: {stdout.decode()}, stderr: {stderr.decode()}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def restart_live_stream(robot_type: RobotType) -> None:
|
|
197
|
+
"""Attempt to restart the Opentrons Live Stream service."""
|
|
198
|
+
if robot_supports_livestream(robot_type) is False:
|
|
199
|
+
# No-op on OT-2 since we don't have a live stream service there
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
command = ["systemctl", "restart", "opentrons-live-stream"]
|
|
203
|
+
subprocess = await asyncio.create_subprocess_exec(
|
|
204
|
+
*command,
|
|
205
|
+
stdout=asyncio.subprocess.PIPE,
|
|
206
|
+
stderr=asyncio.subprocess.PIPE,
|
|
207
|
+
)
|
|
208
|
+
stdout, stderr = await subprocess.communicate()
|
|
209
|
+
if subprocess.returncode == 0:
|
|
210
|
+
log.info("Restarted opentrons-live-stream service.")
|
|
211
|
+
else:
|
|
212
|
+
log.error(
|
|
213
|
+
f"Failed to restart opentrons-live-stream, returncode:{ subprocess.returncode}, stdout: {stdout.decode()}, stderr: {stderr.decode()}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def load_stream_configuration_file_data() -> dict[str, str] | None:
|
|
218
|
+
"""Load the Opentrons Live Stream Conf file and return parsed data or None if an error occurs."""
|
|
219
|
+
src = get_stream_configuration_filepath()
|
|
220
|
+
if not src.exists():
|
|
221
|
+
log.error(f"Opentrons Live Stream configuration file not found: {src}")
|
|
222
|
+
return None
|
|
223
|
+
with src.open("rb") as fd:
|
|
224
|
+
try:
|
|
225
|
+
return parse_stream_configuration_file_data(fd.read())
|
|
226
|
+
except Exception as e:
|
|
227
|
+
log.error(
|
|
228
|
+
f"Opentrons Live Stream status update file parsing failed with: {e}"
|
|
229
|
+
)
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def parse_stream_configuration_file_data(data: bytes) -> Dict[str, str] | None:
|
|
234
|
+
"""
|
|
235
|
+
Parse a collect of bytes for Opentrons Live Stream Configuration data and return a dictionary of
|
|
236
|
+
results keyed by configuration constants. Returns None if an error occurred during parsing.
|
|
237
|
+
"""
|
|
238
|
+
contents: Dict[str, str] = {
|
|
239
|
+
key.decode("utf-8"): val.decode("utf-8")
|
|
240
|
+
for key, val in [line.split(b"=") for line in data.split(b"\n") if b"=" in line]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
enum_stream_keys = {stream_key.value for stream_key in StreamConfigurationKeys}
|
|
244
|
+
if sorted(list(contents.keys())) != sorted(enum_stream_keys):
|
|
245
|
+
log.error(
|
|
246
|
+
"Opentrons Live Stream Configuration file data is incorrect or missing."
|
|
247
|
+
)
|
|
248
|
+
# We don't want to write bad or incomplete data to the file
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
# Migrate old camera default file data to new uniform default
|
|
252
|
+
if contents[StreamConfigurationKeys.SOURCE] == "NONE":
|
|
253
|
+
contents[StreamConfigurationKeys.SOURCE] = DEFAULT_SYSTEM_CAMERA
|
|
254
|
+
return contents
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def write_stream_configuration_file_data(data: Dict[str, str]) -> None:
|
|
258
|
+
src = get_stream_configuration_filepath()
|
|
259
|
+
if not src.exists():
|
|
260
|
+
log.error(f"Opentrons Live Stream configuration file not found: {src}")
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
enum_stream_keys = {stream_key.value for stream_key in StreamConfigurationKeys}
|
|
264
|
+
if sorted(list(data.keys())) != sorted(enum_stream_keys):
|
|
265
|
+
log.error(
|
|
266
|
+
"Data provided to write is not compatible with Opentrons Live Stream Configuration file."
|
|
267
|
+
)
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
with src.open("w") as fd:
|
|
271
|
+
file_lines = [
|
|
272
|
+
f"{StreamConfigurationKeys.BOOT_ID}={data[StreamConfigurationKeys.BOOT_ID]}\n",
|
|
273
|
+
f"{StreamConfigurationKeys.STATUS}={data[StreamConfigurationKeys.STATUS]}\n",
|
|
274
|
+
f"{StreamConfigurationKeys.SOURCE}={data[StreamConfigurationKeys.SOURCE]}\n",
|
|
275
|
+
f"{StreamConfigurationKeys.RESOLUTION}={data[StreamConfigurationKeys.RESOLUTION]}\n",
|
|
276
|
+
f"{StreamConfigurationKeys.FRAMERATE}={data[StreamConfigurationKeys.FRAMERATE]}\n",
|
|
277
|
+
f"{StreamConfigurationKeys.BITRATE}={data[StreamConfigurationKeys.BITRATE]}\n",
|
|
278
|
+
]
|
|
279
|
+
fd.writelines(file_lines)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def image_capture( # noqa: C901
|
|
283
|
+
robot_type: RobotType, parameters: ImageParameters
|
|
284
|
+
) -> bytes | CameraError:
|
|
285
|
+
"""Process an Image Capture request with a Camera utilizing a given set of parameters."""
|
|
286
|
+
camera = DEFAULT_SYSTEM_CAMERA
|
|
287
|
+
|
|
288
|
+
# We must always validate the camera exists
|
|
289
|
+
if not os.path.exists(camera):
|
|
290
|
+
return CameraError(
|
|
291
|
+
message=f"No video device found with device path {camera}", code=None
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if parameters.zoom is not None and (
|
|
295
|
+
parameters.zoom < ZOOM_MIN or parameters.zoom > ZOOM_MAX
|
|
296
|
+
):
|
|
297
|
+
potential_invalid_param = "Zoom"
|
|
298
|
+
elif parameters.contrast is not None and (
|
|
299
|
+
parameters.contrast < CONTRAST_MIN or parameters.contrast > CONTRAST_MAX
|
|
300
|
+
):
|
|
301
|
+
potential_invalid_param = "Contrast"
|
|
302
|
+
elif parameters.brightness is not None and (
|
|
303
|
+
parameters.brightness < BRIGHTNESS_MIN or parameters.brightness > BRIGHTNESS_MAX
|
|
304
|
+
):
|
|
305
|
+
potential_invalid_param = "Brightness"
|
|
306
|
+
elif parameters.saturation is not None and (
|
|
307
|
+
parameters.saturation < SATURATION_MIN or parameters.saturation > SATURATION_MAX
|
|
308
|
+
):
|
|
309
|
+
potential_invalid_param = "Saturation"
|
|
310
|
+
elif parameters.resolution is not None and (
|
|
311
|
+
parameters.resolution[0] < RESOLUTION_MIN[0]
|
|
312
|
+
or parameters.resolution[1] < RESOLUTION_MIN[1]
|
|
313
|
+
or parameters.resolution[0] > RESOLUTION_MAX[0]
|
|
314
|
+
or parameters.resolution[1] > RESOLUTION_MAX[1]
|
|
315
|
+
):
|
|
316
|
+
potential_invalid_param = "Resolution"
|
|
317
|
+
else:
|
|
318
|
+
potential_invalid_param = None
|
|
319
|
+
|
|
320
|
+
if potential_invalid_param is not None:
|
|
321
|
+
return CameraError(
|
|
322
|
+
message=f"{potential_invalid_param} parameter is outside the boundaries allowed for image capture.",
|
|
323
|
+
code="IMAGE_SETTINGS",
|
|
324
|
+
)
|
|
325
|
+
try:
|
|
326
|
+
# Always stop the live stream service to ensure the Camera is always free when attempting an image capture
|
|
327
|
+
await stop_live_stream(robot_type)
|
|
328
|
+
|
|
329
|
+
zoom = parameters.zoom if parameters.zoom is not None else ZOOM_DEFAULT
|
|
330
|
+
contrast = (
|
|
331
|
+
parameters.contrast if parameters.contrast is not None else CONTRAST_DEFAULT
|
|
332
|
+
)
|
|
333
|
+
brightness = (
|
|
334
|
+
parameters.brightness
|
|
335
|
+
if parameters.brightness is not None
|
|
336
|
+
else BRIGHTNESS_DEFAULT
|
|
337
|
+
)
|
|
338
|
+
saturation = (
|
|
339
|
+
parameters.saturation
|
|
340
|
+
if parameters.saturation is not None
|
|
341
|
+
else SATURATION_DEFAULT
|
|
342
|
+
)
|
|
343
|
+
resolution = (
|
|
344
|
+
parameters.resolution
|
|
345
|
+
if parameters.resolution is not None
|
|
346
|
+
else RESOLUTION_DEFAULT
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
result = await ffmpeg.ffmpeg_capture_image_bytes(
|
|
350
|
+
robot_type=robot_type,
|
|
351
|
+
resolution=resolution,
|
|
352
|
+
camera=camera,
|
|
353
|
+
zoom=zoom,
|
|
354
|
+
pan=parameters.pan if parameters.pan is not None else (0, 0),
|
|
355
|
+
contrast=contrast,
|
|
356
|
+
brightness=brightness,
|
|
357
|
+
saturation=saturation,
|
|
358
|
+
)
|
|
359
|
+
except Exception:
|
|
360
|
+
result = CameraError(
|
|
361
|
+
message="Exception occured during execution of system image capture.",
|
|
362
|
+
code=None,
|
|
363
|
+
)
|
|
364
|
+
finally:
|
|
365
|
+
# Restart the live stream service
|
|
366
|
+
await restart_live_stream(robot_type)
|
|
367
|
+
return result
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@lru_cache(maxsize=1)
|
|
371
|
+
def get_boot_id() -> str:
|
|
372
|
+
if IS_ROBOT:
|
|
373
|
+
return Path("/proc/sys/kernel/random/boot_id").read_text().strip()
|
|
374
|
+
else:
|
|
375
|
+
return "SIMULATED_BOOT_ID"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def camera_exists() -> bool:
|
|
379
|
+
"""Validate whether or not the camera device exists."""
|
|
380
|
+
return os.path.exists(DEFAULT_SYSTEM_CAMERA)
|
|
381
|
+
# todo(chb, 2025-11-10): Eventually when we support multiple cameras this should accept a camera parameter to check for
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""opentrons.system.ffmpeg: Functions and data for interacting with FFMPEG."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
from opentrons.protocol_engine.resources.camera_provider import CameraError
|
|
6
|
+
from opentrons_shared_data.robot.types import RobotType
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
# === FFMPEG Filter Details ===
|
|
11
|
+
# The following filters are utilized via the '-vf' flag to manipulate the final image returned:
|
|
12
|
+
# 'crop' = [output_width]:[output_height]:x:y
|
|
13
|
+
# The crop is composed of a desired output width and height for the image, and
|
|
14
|
+
# an X/Y position to begin the crop at (becomes the top left of the new image).
|
|
15
|
+
# 'scale' = [width]:[height]
|
|
16
|
+
# The resolution of the final image to export, scales up or down based on configuration.
|
|
17
|
+
# 'lut' (Look-Up Table) = 'y' (Luminance) = 'val' (Current value of a given pixel from 0-255)
|
|
18
|
+
# The equation on the Look up table takes the current luminance value of an image per pixel
|
|
19
|
+
# and manipulates it using Contrast and Brightness settings. This is applied to the whole image.
|
|
20
|
+
# 'hue' (Image color range) = 's' (Saturation) = [range]
|
|
21
|
+
# The hue flag uses the 's' (saturation) modifier to scale image color intensity, default is 1.
|
|
22
|
+
|
|
23
|
+
# todo(chb, 2025-10-13): Right now we're just zooming towards the center of the frame. The 'pan'
|
|
24
|
+
# setting should be used on the latter half of 'crop' to determine our cropping location instead.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def ffmpeg_capture_image_bytes(
|
|
28
|
+
robot_type: RobotType,
|
|
29
|
+
resolution: Tuple[int, int],
|
|
30
|
+
camera: str,
|
|
31
|
+
zoom: float,
|
|
32
|
+
pan: Tuple[int, int],
|
|
33
|
+
contrast: float,
|
|
34
|
+
brightness: int,
|
|
35
|
+
saturation: float,
|
|
36
|
+
) -> bytes | CameraError:
|
|
37
|
+
"""Execute an FFMPEG command to capture an image based on various image parameters."""
|
|
38
|
+
if robot_type == "OT-2 Standard":
|
|
39
|
+
ot2_brightness: float = (
|
|
40
|
+
brightness / 128
|
|
41
|
+
) * -1 # OT-2's equilizer field takes a value of -1.0 to 1.0 for brightness
|
|
42
|
+
command = [
|
|
43
|
+
"ffmpeg",
|
|
44
|
+
"-hwaccel",
|
|
45
|
+
"auto",
|
|
46
|
+
"-video_size",
|
|
47
|
+
f"{resolution[0]}x{resolution[1]}",
|
|
48
|
+
"-f",
|
|
49
|
+
"v4l2",
|
|
50
|
+
"-i",
|
|
51
|
+
f"{camera}",
|
|
52
|
+
"-vf",
|
|
53
|
+
f"crop=iw/{zoom}:ih/{zoom}:(iw-iw/{zoom})/{zoom}:(ih-ih/{zoom})/{zoom},"
|
|
54
|
+
f"scale={resolution[0]}:{resolution[1]},"
|
|
55
|
+
f"eq=brightness={ot2_brightness}:contrast={contrast}:saturation={saturation}",
|
|
56
|
+
"-frames:v",
|
|
57
|
+
"1",
|
|
58
|
+
"-f",
|
|
59
|
+
"image2pipe",
|
|
60
|
+
"-vcodec",
|
|
61
|
+
"mjpeg",
|
|
62
|
+
"-",
|
|
63
|
+
]
|
|
64
|
+
else:
|
|
65
|
+
command = [
|
|
66
|
+
"ffmpeg",
|
|
67
|
+
"-hwaccel",
|
|
68
|
+
"auto",
|
|
69
|
+
"-video_size",
|
|
70
|
+
f"{resolution[0]}x{resolution[1]}",
|
|
71
|
+
"-f",
|
|
72
|
+
"v4l2",
|
|
73
|
+
"-i",
|
|
74
|
+
f"{camera}",
|
|
75
|
+
"-vf",
|
|
76
|
+
f"crop=iw/{zoom}:ih/{zoom}:(iw-iw/{zoom})/{zoom}:(ih-ih/{zoom})/{zoom},"
|
|
77
|
+
f"scale={resolution[0]}:{resolution[1]},"
|
|
78
|
+
f"lut=y=(val-128)*{contrast}+128-{brightness},"
|
|
79
|
+
f"hue=s={saturation},format=nv12",
|
|
80
|
+
"-frames:v",
|
|
81
|
+
"1",
|
|
82
|
+
"-f",
|
|
83
|
+
"image2pipe",
|
|
84
|
+
"-vcodec",
|
|
85
|
+
"mjpeg",
|
|
86
|
+
"-",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
subprocess = await asyncio.create_subprocess_exec(
|
|
90
|
+
*command,
|
|
91
|
+
stdout=asyncio.subprocess.PIPE,
|
|
92
|
+
stderr=asyncio.subprocess.PIPE,
|
|
93
|
+
)
|
|
94
|
+
stdout: bytes
|
|
95
|
+
stderr: bytes
|
|
96
|
+
stdout, stderr = await subprocess.communicate()
|
|
97
|
+
if subprocess.returncode == 0:
|
|
98
|
+
log.info("Successfully captured an image with camera.")
|
|
99
|
+
# Upon success, dump our byte stream to the result
|
|
100
|
+
return stdout
|
|
101
|
+
else:
|
|
102
|
+
log.error(
|
|
103
|
+
f"Failed to capture an image with camera, returncode:{ subprocess.returncode}, stdout: {stdout.decode()}, stderr: {stderr.decode()}"
|
|
104
|
+
)
|
|
105
|
+
return CameraError(
|
|
106
|
+
message="Failed to return bytes from FFMPEG image capture.",
|
|
107
|
+
code=str(subprocess.returncode)
|
|
108
|
+
if subprocess.returncode is not None
|
|
109
|
+
else None,
|
|
110
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opentrons
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.8.0a8
|
|
4
4
|
Summary: The Opentrons API is a simple framework designed to make writing automated biology lab protocols easy.
|
|
5
5
|
Project-URL: opentrons.com, https://www.opentrons.com
|
|
6
6
|
Project-URL: Source Code On Github, https://github.com/Opentrons/opentrons/tree/edge/api
|
|
@@ -24,7 +24,7 @@ Requires-Dist: click<9,>=8.0.0
|
|
|
24
24
|
Requires-Dist: importlib-metadata>=1.0; python_version < '3.8'
|
|
25
25
|
Requires-Dist: jsonschema<4.18.0,>=3.0.1
|
|
26
26
|
Requires-Dist: numpy<2,>=1.20.0
|
|
27
|
-
Requires-Dist: opentrons-shared-data==8.
|
|
27
|
+
Requires-Dist: opentrons-shared-data==8.8.0a8
|
|
28
28
|
Requires-Dist: packaging>=21.0
|
|
29
29
|
Requires-Dist: pydantic-settings<3,>=2
|
|
30
30
|
Requires-Dist: pydantic<3,>=2.0.0
|
|
@@ -32,6 +32,6 @@ Requires-Dist: pyserial>=3.5
|
|
|
32
32
|
Requires-Dist: pyusb==1.2.1
|
|
33
33
|
Requires-Dist: typing-extensions<5,>=4.0.0
|
|
34
34
|
Provides-Extra: flex-hardware
|
|
35
|
-
Requires-Dist: opentrons-hardware[flex]==8.
|
|
35
|
+
Requires-Dist: opentrons-hardware[flex]==8.8.0a8; extra == 'flex-hardware'
|
|
36
36
|
Provides-Extra: ot2-hardware
|
|
37
|
-
Requires-Dist: opentrons-hardware==8.
|
|
37
|
+
Requires-Dist: opentrons-hardware==8.8.0a8; extra == 'ot2-hardware'
|