opentrons 8.7.0a7__py3-none-any.whl → 8.8.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.

Files changed (109) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +4 -1
  3. opentrons/config/__init__.py +7 -0
  4. opentrons/drivers/asyncio/communication/serial_connection.py +8 -5
  5. opentrons/drivers/flex_stacker/driver.py +6 -1
  6. opentrons/drivers/vacuum_module/__init__.py +5 -0
  7. opentrons/drivers/vacuum_module/abstract.py +93 -0
  8. opentrons/drivers/vacuum_module/driver.py +208 -0
  9. opentrons/drivers/vacuum_module/errors.py +39 -0
  10. opentrons/drivers/vacuum_module/simulator.py +85 -0
  11. opentrons/drivers/vacuum_module/types.py +79 -0
  12. opentrons/execute.py +3 -0
  13. opentrons/hardware_control/backends/flex_protocol.py +2 -0
  14. opentrons/hardware_control/backends/ot3controller.py +35 -2
  15. opentrons/hardware_control/backends/ot3simulator.py +2 -0
  16. opentrons/hardware_control/backends/ot3utils.py +37 -0
  17. opentrons/hardware_control/module_control.py +23 -2
  18. opentrons/hardware_control/modules/mod_abc.py +1 -1
  19. opentrons/hardware_control/modules/types.py +1 -1
  20. opentrons/hardware_control/motion_utilities.py +6 -6
  21. opentrons/hardware_control/ot3api.py +62 -13
  22. opentrons/hardware_control/protocols/gripper_controller.py +1 -0
  23. opentrons/hardware_control/protocols/liquid_handler.py +6 -2
  24. opentrons/hardware_control/types.py +12 -0
  25. opentrons/legacy_commands/commands.py +58 -5
  26. opentrons/legacy_commands/module_commands.py +29 -0
  27. opentrons/legacy_commands/protocol_commands.py +33 -1
  28. opentrons/legacy_commands/types.py +75 -1
  29. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  30. opentrons/protocol_api/_types.py +2 -0
  31. opentrons/protocol_api/core/engine/_default_labware_versions.py +1 -0
  32. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  33. opentrons/protocol_api/core/engine/instrument.py +109 -26
  34. opentrons/protocol_api/core/engine/module_core.py +27 -3
  35. opentrons/protocol_api/core/engine/protocol.py +33 -1
  36. opentrons/protocol_api/core/engine/stringify.py +2 -0
  37. opentrons/protocol_api/core/instrument.py +19 -2
  38. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  39. opentrons/protocol_api/core/legacy/legacy_module_core.py +15 -4
  40. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +12 -0
  41. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  42. opentrons/protocol_api/core/module.py +25 -2
  43. opentrons/protocol_api/core/protocol.py +12 -0
  44. opentrons/protocol_api/instrument_context.py +388 -2
  45. opentrons/protocol_api/labware.py +5 -2
  46. opentrons/protocol_api/module_contexts.py +133 -30
  47. opentrons/protocol_api/protocol_context.py +61 -17
  48. opentrons/protocol_api/robot_context.py +3 -4
  49. opentrons/protocol_api/validation.py +43 -2
  50. opentrons/protocol_engine/__init__.py +4 -0
  51. opentrons/protocol_engine/actions/__init__.py +2 -0
  52. opentrons/protocol_engine/actions/actions.py +9 -0
  53. opentrons/protocol_engine/commands/__init__.py +14 -0
  54. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  55. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  56. opentrons/protocol_engine/commands/capture_image.py +302 -0
  57. opentrons/protocol_engine/commands/command.py +1 -0
  58. opentrons/protocol_engine/commands/command_unions.py +13 -0
  59. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  60. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  61. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  62. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +1 -1
  63. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  64. opentrons/protocol_engine/commands/move_labware.py +3 -4
  65. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  66. opentrons/protocol_engine/commands/movement_common.py +29 -2
  67. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  68. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +12 -9
  69. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +17 -12
  70. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +1 -1
  71. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  72. opentrons/protocol_engine/engine_support.py +3 -0
  73. opentrons/protocol_engine/errors/__init__.py +8 -0
  74. opentrons/protocol_engine/errors/exceptions.py +64 -0
  75. opentrons/protocol_engine/execution/__init__.py +2 -0
  76. opentrons/protocol_engine/execution/command_executor.py +54 -1
  77. opentrons/protocol_engine/execution/create_queue_worker.py +4 -1
  78. opentrons/protocol_engine/execution/labware_movement.py +13 -4
  79. opentrons/protocol_engine/execution/pipetting.py +19 -25
  80. opentrons/protocol_engine/protocol_engine.py +62 -2
  81. opentrons/protocol_engine/resources/__init__.py +2 -0
  82. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  83. opentrons/protocol_engine/resources/file_provider.py +133 -58
  84. opentrons/protocol_engine/slot_standardization.py +2 -0
  85. opentrons/protocol_engine/state/camera.py +54 -0
  86. opentrons/protocol_engine/state/commands.py +24 -4
  87. opentrons/protocol_engine/state/geometry.py +68 -10
  88. opentrons/protocol_engine/state/labware.py +10 -6
  89. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +6 -1
  90. opentrons/protocol_engine/state/modules.py +9 -0
  91. opentrons/protocol_engine/state/preconditions.py +59 -0
  92. opentrons/protocol_engine/state/state.py +30 -0
  93. opentrons/protocol_engine/state/state_summary.py +2 -0
  94. opentrons/protocol_engine/state/update_types.py +10 -0
  95. opentrons/protocol_engine/types/__init__.py +14 -1
  96. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  97. opentrons/protocol_engine/types/location.py +26 -2
  98. opentrons/protocol_engine/types/module.py +1 -1
  99. opentrons/protocol_runner/protocol_runner.py +14 -1
  100. opentrons/protocol_runner/run_orchestrator.py +31 -0
  101. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  102. opentrons/simulate.py +3 -0
  103. opentrons/system/camera.py +333 -3
  104. opentrons/system/ffmpeg.py +110 -0
  105. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
  106. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +109 -97
  107. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
  108. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
  109. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,68 @@
1
1
  import asyncio
2
2
  import os
3
3
  from pathlib import Path
4
-
5
- from opentrons.config import ARCHITECTURE, SystemArchitecture
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
- """Take a picture and save it to filename
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
@@ -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.7.0a7
3
+ Version: 8.8.0a7
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.7.0a7
27
+ Requires-Dist: opentrons-shared-data==8.8.0a7
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.7.0a7; extra == 'flex-hardware'
35
+ Requires-Dist: opentrons-hardware[flex]==8.8.0a7; extra == 'flex-hardware'
36
36
  Provides-Extra: ot2-hardware
37
- Requires-Dist: opentrons-hardware==8.7.0a7; extra == 'ot2-hardware'
37
+ Requires-Dist: opentrons-hardware==8.8.0a7; extra == 'ot2-hardware'