Skip to content

Commit caf1a07

Browse files
committed
Merge back 'chore_release-8.8.0' into 'edge' (#20140)
Resolved conflicts: - app/src/organisms/ODD/QuickTransferFlow/utils/createQuickTransferFile.ts
2 parents 995d418 + 27e5059 commit caf1a07

File tree

35 files changed

+328
-80
lines changed

35 files changed

+328
-80
lines changed

.github/workflows/api-test-lint-deploy.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ jobs:
214214
- uses: './.github/actions/python/setup'
215215
with:
216216
project: 'api'
217+
- name: 'build api distributables'
218+
shell: bash
219+
run: make -C api sdist wheel
220+
217221
# creds and repository configuration for deploying python wheels
218222
- if: ${{ !env.OT_TAG }}
219223
name: 'upload to test pypi'

.github/workflows/shared-data-test-lint-deploy.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ jobs:
184184
script: |
185185
const { buildComplexEnvVars, } = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/utils.js`)
186186
buildComplexEnvVars(core, context)
187+
- name: 'build shared-data wheel'
188+
shell: bash
189+
run: make -C shared-data dist-py
190+
187191
# creds and repository configuration for deploying python wheels
188192
- if: ${{ !env.OT_TAG }}
189193
name: 'upload to test pypi'

api/src/opentrons/hardware_control/backends/ot3controller.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,11 +664,11 @@ def _build_move_node_axis_runner(
664664
return None, False
665665
# Create a target that doesn't incorporate the plunger into a joint axis with the gantry
666666
plunger_axes = [Axis.P_L, Axis.P_R]
667-
move_target = self._move_manager.devectorize_axes(
668-
origin, target, speed, plunger_axes
669-
)
670667

671668
try:
669+
move_target = self._move_manager.devectorize_axes(
670+
origin, target, speed, plunger_axes
671+
)
672672
_, movelist = self._move_manager.plan_motion(
673673
origin=origin, target_list=[move_target]
674674
)

api/src/opentrons/protocol_api/labware.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -822,8 +822,8 @@ def set_calibration(self, delta: Point) -> None:
822822
def set_offset(self, x: float, y: float, z: float) -> None:
823823
"""Set the labware's position offset.
824824
825-
The offset is an x, y, z vector in deck coordinates
826-
(see :ref:`protocol-api-deck-coords`).
825+
An offset of `(x=0, y=0, z=0)` means the labware's uncalibrated position before
826+
any offset from Labware Position Check is applied.
827827
828828
How the motion system applies the offset depends on the API level of the protocol.
829829
@@ -882,6 +882,9 @@ def set_offset(self, x: float, y: float, z: float) -> None:
882882
def calibrated_offset(self) -> Point:
883883
"""The front-left-bottom corner of the labware, including its labware offset.
884884
885+
The offset is an x, y, z vector in deck coordinates
886+
(see :ref:`protocol-api-deck-coords`).
887+
885888
When running a protocol in the Opentrons App or on the touchscreen, Labware
886889
Position Check sets the labware offset.
887890
"""

api/src/opentrons/protocol_engine/commands/capture_image.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
2020
from ..errors import (
2121
CameraDisabledError,
22+
CameraSettingsInvalidError,
2223
FileNameInvalidError,
2324
)
2425
from ..errors.error_occurrence import ErrorOccurrence
@@ -193,6 +194,26 @@ async def execute(
193194
message=f"Capture image filename cannot contain character(s): {SPECIAL_CHARACTERS.intersection(set(params.fileName))}"
194195
)
195196

197+
# Validate the image filter parameters
198+
if params.brightness is not None and (
199+
params.brightness < 0 or params.brightness > 100
200+
):
201+
raise CameraSettingsInvalidError(
202+
message="Capture image brightness must be a percentage from 0% to 100%."
203+
)
204+
if params.contrast is not None and (
205+
params.contrast < 0 or params.contrast > 100
206+
):
207+
raise CameraSettingsInvalidError(
208+
message="Capture image contrast must be a percentage from 0% to 100%."
209+
)
210+
if params.saturation is not None and (
211+
params.saturation < 0 or params.saturation > 100
212+
):
213+
raise CameraSettingsInvalidError(
214+
message="Capture image saturation must be a percentage from 0% to 100%."
215+
)
216+
196217
# Handle capturing an image with the CameraProvider - Engine camera settings take priority
197218
camera_settings = await self._camera_provider.get_camera_settings()
198219
engine_camera_settings = self._state_view.camera.get_enablement_settings()

api/src/opentrons/protocol_engine/execution/command_executor.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ async def execute(self, command_id: str) -> None:
155155
log.debug(
156156
f"Executing {running_command.id}, {running_command.commandType}, {running_command.params}"
157157
)
158+
error_occurred = False
158159
try:
159160
result = await command_impl.execute(
160161
running_command.params # type: ignore[arg-type]
@@ -191,7 +192,7 @@ async def execute(self, command_id: str) -> None:
191192
type=error_recovery_type,
192193
)
193194
)
194-
await self.capture_error_image(running_command)
195+
error_occurred = True
195196

196197
else:
197198
if isinstance(result, SuccessData):
@@ -227,6 +228,10 @@ async def execute(self, command_id: str) -> None:
227228
type=error_recovery_type,
228229
)
229230
)
231+
error_occurred = True
232+
finally:
233+
# Handle error image capture if appropriate
234+
if error_occurred:
230235
await self.capture_error_image(running_command)
231236

232237
def cancel_tasks(self, message: str | None = None) -> None:
@@ -236,7 +241,10 @@ def cancel_tasks(self, message: str | None = None) -> None:
236241
async def capture_error_image(self, running_command: Command) -> None:
237242
"""Capture an image of an error event."""
238243
try:
239-
camera_enablement = await self._camera_provider.get_camera_settings()
244+
camera_enablement = self._state_store.camera.get_enablement_settings()
245+
if camera_enablement is None:
246+
# Utilize the global camera settings
247+
camera_enablement = await self._camera_provider.get_camera_settings()
240248
# Only capture photos of errors if the setting to do so is enabled
241249
if (
242250
camera_enablement.cameraEnabled

api/src/opentrons/protocol_engine/resources/file_provider.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"<",
2323
">",
2424
"*",
25-
" ",
2625
"$",
2726
"!",
2827
"?",

api/src/opentrons/system/camera.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
]
3838

3939
# Camera Parameter Globals
40+
RESOLUTION_MIN = (320, 240)
41+
RESOLUTION_MAX = (7680, 4320)
4042
RESOLUTION_DEFAULT = (1920, 1080)
4143
ZOOM_MIN = 1.0
4244
ZOOM_MAX = 2.0
@@ -277,7 +279,7 @@ def write_stream_configuration_file_data(data: Dict[str, str]) -> None:
277279
fd.writelines(file_lines)
278280

279281

280-
async def image_capture(
282+
async def image_capture( # noqa: C901
281283
robot_type: RobotType, parameters: ImageParameters
282284
) -> bytes | CameraError:
283285
"""Process an Image Capture request with a Camera utilizing a given set of parameters."""
@@ -305,8 +307,16 @@ async def image_capture(
305307
parameters.saturation < SATURATION_MIN or parameters.saturation > SATURATION_MAX
306308
):
307309
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"
308317
else:
309318
potential_invalid_param = None
319+
310320
if potential_invalid_param is not None:
311321
return CameraError(
312322
message=f"{potential_invalid_param} parameter is outside the boundaries allowed for image capture.",
@@ -363,3 +373,9 @@ def get_boot_id() -> str:
363373
return Path("/proc/sys/kernel/random/boot_id").read_text().strip()
364374
else:
365375
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

api/tests/opentrons/hardware_control/backends/test_ot3_controller.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,32 @@ def move_group_run_side_effect(
14541454
},
14551455
None,
14561456
],
1457+
[
1458+
{
1459+
Axis.X: 0,
1460+
Axis.Y: 0,
1461+
Axis.Z_L: 0,
1462+
Axis.Z_R: 0,
1463+
Axis.P_L: 0,
1464+
},
1465+
{
1466+
Axis.X: 0, # Zero Length Move, make sure it doesn't raise an error
1467+
Axis.Y: 0,
1468+
Axis.Z_L: 0,
1469+
Axis.P_L: 0,
1470+
},
1471+
{
1472+
Axis.X: 0,
1473+
Axis.Y: 0,
1474+
Axis.Z_L: 0,
1475+
Axis.Z_R: 0,
1476+
Axis.P_L: 0,
1477+
Axis.P_R: 0,
1478+
Axis.Z_G: 0,
1479+
Axis.G: 0,
1480+
},
1481+
None,
1482+
],
14571483
],
14581484
)
14591485
async def test_controller_move(

api/tests/opentrons/protocol_engine/commands/test_capture_image.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,8 @@ async def test_ensure_camera_used_precondition_set(
240240
50,
241241
],
242242
[
243-
(1, 2),
244-
(1, 2),
243+
(320, 240),
244+
(320, 240),
245245
1.5,
246246
1.5,
247247
(3, 4),
@@ -254,8 +254,8 @@ async def test_ensure_camera_used_precondition_set(
254254
75,
255255
],
256256
[
257-
(1, 2),
258-
(1, 2),
257+
(320, 240),
258+
(320, 240),
259259
2.0,
260260
2.0,
261261
(3, 4),
@@ -268,8 +268,8 @@ async def test_ensure_camera_used_precondition_set(
268268
25,
269269
],
270270
[
271-
(9999999, 9999999),
272-
(9999999, 9999999),
271+
(7680, 4320),
272+
(7680, 4320),
273273
1.0,
274274
1.0,
275275
(25, 45),
@@ -389,3 +389,56 @@ async def test_raises_filename_error(
389389
params = CaptureImageParams(fileName="badname" + char)
390390
with pytest.raises(FileNameInvalidError):
391391
await subject.execute(params=params)
392+
393+
394+
@pytest.mark.parametrize(
395+
argnames=[
396+
"zoom",
397+
"contrast",
398+
"brightness",
399+
"saturation",
400+
"resolution",
401+
],
402+
argvalues=[
403+
[0.9, 1, 1, 1, (1920, 1080)],
404+
[2.1, 1, 1, 1, (1920, 1080)],
405+
[1, -1, 1, 1, (1920, 1080)],
406+
[1, 101, 1, 1, (1920, 1080)],
407+
[1, 1, -1, 1, (1920, 1080)],
408+
[1, 1, 101, 1, (1920, 1080)],
409+
[1, 1, 1, -1, (1920, 1080)],
410+
[1, 1, 1, 101, (1920, 1080)],
411+
[1, 1, 1, 1, (0, 0)],
412+
[1, 1, 1, 1, (10000, 10000)],
413+
],
414+
)
415+
async def test_raises_image_parameter_error(
416+
decoy: Decoy,
417+
state_view: StateView,
418+
file_provider: FileProvider,
419+
camera_provider_image_capture: CameraProvider,
420+
zoom: float,
421+
contrast: float,
422+
brightness: float,
423+
saturation: float,
424+
resolution: Tuple[int, int],
425+
) -> None:
426+
"""It should raise CameraSettingsInvalidError when the capture image command is provided bad filter params."""
427+
subject = CaptureImageImpl(
428+
state_view=state_view,
429+
file_provider=file_provider,
430+
camera_provider=camera_provider_image_capture,
431+
)
432+
params = CaptureImageParams(
433+
resolution=resolution,
434+
zoom=zoom,
435+
contrast=contrast,
436+
brightness=brightness,
437+
saturation=saturation,
438+
)
439+
440+
decoy.when(state_view.files.get_filecount()).then_return(0)
441+
442+
with mock.patch("os.path.exists", mock.Mock(return_value=True)):
443+
with pytest.raises(CameraSettingsInvalidError):
444+
await subject.execute(params=params)

0 commit comments

Comments
 (0)