Skip to content

Commit cc75caf

Browse files
committed
Merge back 'chore_release-8.8.0' into 'chore_release-pd-8.7.0' (#20304)
2 parents 245073e + c2a71c8 commit cc75caf

File tree

10 files changed

+248
-17
lines changed

10 files changed

+248
-17
lines changed

api/src/opentrons/system/camera.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ async def update_live_stream_status(
158158
and camera_enable_settings.liveStreamEnabled
159159
):
160160
# Check to see if the camera device is available
161-
raw_device = str(contents["SOURCE"])[1:-1]
161+
raw_device = str(contents["SOURCE"])
162162
if not os.path.exists(raw_device):
163163
log.error(
164164
f"Opentrons Live Stream cannot sample the camera. No video device found with device path: {raw_device}"

app-shell/build/release-notes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ Welcome to the v8.8.0 release of the Opentrons App! This release includes concur
2424
- An attached pipette no longer descends to attach a calibration probe for Labware Position Check, creating more clearance on the deck.
2525
- Changed runtime parameters no longer revert to their default values.
2626

27+
### Known Issues
28+
29+
- Error recovery in the app or on the Flex touchscreen can't successfully resolve overpressure errors that occur during a dynamic aspirate or dispense. We recommend canceling the protocol when these errors occur.
30+
- Images captured in a protocol are not available during the run via USB. Use a different connection type (Wi-Fi or Ethernet), view the images on the touchscreen, or download the images after the run is complete.
31+
2732
---
2833

2934
## Opentrons App Changes in 8.7.0

app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ export function getErrorKind(
2626
case 'prepareToAspirate':
2727
return ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE
2828
case 'aspirate':
29-
case 'aspirateInPlace': {
29+
case 'aspirateInPlace':
30+
case 'aspirateWhileTracking': {
3031
return ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING
3132
}
3233
case 'dispense':
3334
case 'dispenseInPlace':
35+
case 'dispenseWhileTracking':
3436
case 'blowout':
3537
case 'blowOutInPlace':
3638
return ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING

robot-server/robot_server/app_setup.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from fastapi import FastAPI
77
from fastapi.middleware.cors import CORSMiddleware
8+
from fastapi.openapi.docs import get_redoc_html
9+
from fastapi.responses import HTMLResponse
810

911
from server_utils.fastapi_utils.server_timing_middleware import server_timing_middleware
1012

@@ -36,6 +38,9 @@
3638
)
3739

3840

41+
_REDOC_CDN_URL = "https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"
42+
43+
3944
@contextlib.asynccontextmanager
4045
async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
4146
"""The server's startup and shutdown logic.
@@ -108,9 +113,31 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
108113
# Disable documentation hosting via Swagger UI, normally at /docs.
109114
# We instead focus on the docs hosted by ReDoc, at /redoc.
110115
docs_url=None,
116+
# redoc_url is replaced by our own /redoc router, below.
117+
redoc_url=None,
111118
lifespan=_lifespan,
112119
)
113120

121+
122+
# This is a workaround for a broken /redoc page in versions of FastAPI <0.115.3.
123+
# The page loads Redoc from a CDN, and the problem is that the default CDN URL
124+
# uses a fragile version tag.
125+
# https://github.com/Redocly/redoc/issues/2743
126+
# https://github.com/fastapi/fastapi/pull/9700
127+
@app.get("/redoc", include_in_schema=False)
128+
async def redoc_html() -> HTMLResponse: # noqa: D103
129+
if app.openapi_url is None:
130+
raise RuntimeError(
131+
"Couldn't get OpenAPI URL from FastAPI."
132+
+ " This is probably some kind of misconfiguration."
133+
)
134+
return get_redoc_html(
135+
openapi_url=app.openapi_url,
136+
title=app.title,
137+
redoc_js_url=_REDOC_CDN_URL,
138+
)
139+
140+
114141
app.add_middleware(
115142
CORSMiddleware,
116143
allow_origins=("*"),

robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
from opentrons.protocol_engine.types import DeckConfigurationType
2020

2121
from robot_server.service.notifications import MaintenanceRunsPublisher
22+
from opentrons.protocol_engine.resources.camera_provider import (
23+
CameraProvider,
24+
CameraSettings,
25+
)
26+
from opentrons.system import camera
2227

2328

2429
def _build_run(
@@ -92,6 +97,7 @@ async def create(
9297
labware_offsets: Sequence[LabwareOffsetCreate | LegacyLabwareOffsetCreate],
9398
deck_configuration: DeckConfigurationType,
9499
notify_publishers: Callable[[], None],
100+
camera_provider: CameraProvider,
95101
) -> MaintenanceRun:
96102
"""Create a new, current maintenance run.
97103
@@ -100,6 +106,7 @@ async def create(
100106
created_at: Creation datetime.
101107
labware_offsets: Labware offsets to initialize the engine with.
102108
notify_publishers: Utilized by the engine to notify publishers of state changes.
109+
camera_provider: Utility for accessing image capture and camera settings.
103110
104111
Returns:
105112
The run resource.
@@ -115,6 +122,13 @@ async def create(
115122
notify_publishers=notify_publishers,
116123
)
117124

125+
await camera.update_live_stream_status(
126+
self._run_orchestrator_store._robot_type,
127+
True,
128+
camera_provider,
129+
state_summary.cameraSettings,
130+
)
131+
118132
maintenance_run_data = _build_run(
119133
run_id=run_id,
120134
created_at=created_at,
@@ -151,11 +165,18 @@ def get(self, run_id: str) -> MaintenanceRun:
151165
state_summary=state_summary,
152166
)
153167

154-
async def delete(self, run_id: str) -> None:
168+
async def delete(
169+
self,
170+
run_id: str,
171+
camera_settings: CameraSettings | None,
172+
camera_provider: CameraProvider,
173+
) -> None:
155174
"""Delete a maintenance run.
156175
157176
Args:
158177
run_id: The identifier of the run to remove.
178+
camera_settings: Optional set of camera settings form an external run. If None do not attempt to restart the live stream.
179+
camera_provider: Utility for accessing image capture and camera settings.
159180
160181
Raises:
161182
RunConflictError: If deleting the current run, the current run
@@ -166,6 +187,15 @@ async def delete(self, run_id: str) -> None:
166187
await self._run_orchestrator_store.clear()
167188
await self._maintenance_runs_publisher.publish_current_maintenance_run_async()
168189

190+
if camera_settings is not None:
191+
# Restart the live stream for the external run when the maintenance run has ended.
192+
await camera.update_live_stream_status(
193+
self._run_orchestrator_store._robot_type,
194+
True,
195+
camera_provider,
196+
camera_settings,
197+
)
198+
169199
else:
170200
raise MaintenanceRunNotFoundError(run_id=run_id)
171201

robot-server/robot_server/maintenance_runs/router/base_router.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,19 @@
3535
from ..maintenance_run_orchestrator_store import RunConflictError
3636
from ..maintenance_run_data_manager import MaintenanceRunDataManager
3737
from ..dependencies import get_maintenance_run_data_manager
38+
from robot_server.runs.dependencies import get_run_data_manager
39+
from robot_server.runs.run_data_manager import RunDataManager
3840

3941
from robot_server.deck_configuration.fastapi_dependencies import (
4042
get_deck_configuration_store,
4143
)
4244
from robot_server.deck_configuration.store import DeckConfigurationStore
4345
from robot_server.service.notifications import get_pe_notify_publishers
46+
from robot_server.camera.fastapi_dependencies import (
47+
get_camera_provider,
48+
)
49+
from opentrons.protocol_engine.resources.camera_provider import CameraProvider
50+
from opentrons.protocol_engine.types import EngineStatus
4451

4552
log = logging.getLogger(__name__)
4653
base_router = LightRouter()
@@ -157,6 +164,7 @@ async def create_run(
157164
DeckConfigurationStore, Depends(get_deck_configuration_store)
158165
],
159166
notify_publishers: Annotated[Callable[[], None], Depends(get_pe_notify_publishers)],
167+
camera_provider: Annotated[CameraProvider, Depends(get_camera_provider)],
160168
request_body: Optional[RequestModel[MaintenanceRunCreate]] = None,
161169
) -> PydanticResponse[SimpleBody[MaintenanceRun]]:
162170
"""Create a new maintenance run.
@@ -169,6 +177,7 @@ async def create_run(
169177
is_ok_to_create_maintenance_run: Verify if a maintenance run may be created if a protocol run exists.
170178
check_estop: Dependency to verify the estop is in a valid state.
171179
deck_configuration_store: Dependency to fetch the deck configuration.
180+
camera_provider: Dependency to provide access to the Camera Settings to the run.
172181
notify_publishers: Utilized by the engine to notify publishers of state changes.
173182
"""
174183
if not is_ok_to_create_maintenance_run:
@@ -185,6 +194,7 @@ async def create_run(
185194
labware_offsets=offsets,
186195
deck_configuration=deck_configuration,
187196
notify_publishers=notify_publishers,
197+
camera_provider=camera_provider,
188198
)
189199

190200
log.info(f'Created an empty run "{run_id}"".')
@@ -267,18 +277,36 @@ async def get_run(
267277
)
268278
async def remove_run(
269279
runId: str,
270-
run_data_manager: Annotated[
280+
maintenance_run_data_manager: Annotated[
271281
MaintenanceRunDataManager, Depends(get_maintenance_run_data_manager)
272282
],
283+
run_data_manager: Annotated[RunDataManager, Depends(get_run_data_manager)],
284+
camera_provider: Annotated[CameraProvider, Depends(get_camera_provider)],
273285
) -> PydanticResponse[SimpleEmptyBody]:
274286
"""Delete a maintenance run by its ID.
275287
276288
Arguments:
277289
runId: Run ID pulled from URL.
290+
maintenance_run_data_manager: Current maintenance run data management.
278291
run_data_manager: Current run data management.
292+
camera_provider: Utility for accessing image capture and camera settings.
279293
"""
280294
try:
281-
await run_data_manager.delete(runId)
295+
camera_settings = None
296+
if run_data_manager.current_run_id is not None:
297+
# Import the camera settings from an external run if one exists
298+
state_summary = run_data_manager._get_good_state_summary(
299+
run_data_manager.current_run_id
300+
)
301+
if (
302+
state_summary is not None
303+
and state_summary.status is not EngineStatus.FINISHING
304+
):
305+
camera_settings = state_summary.cameraSettings
306+
307+
await maintenance_run_data_manager.delete(
308+
runId, camera_settings, camera_provider
309+
)
282310

283311
except RunConflictError as e:
284312
raise RunNotIdle().as_error(status.HTTP_409_CONFLICT) from e

robot-server/robot_server/runs/run_data_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ async def create(
207207
created_at: Creation datetime.
208208
labware_offsets: Labware offsets to initialize the engine with.
209209
deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with.
210+
camera_provider: Utility for accessing image capture and camera settings.
210211
notify_publishers: Utilized by the engine to notify publishers of state changes.
211212
run_time_param_values: Any runtime parameter values to set.
212213
run_time_param_paths: Any runtime filepath to set.

robot-server/tests/maintenance_runs/router/conftest.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from robot_server.maintenance_runs.maintenance_run_data_manager import (
99
MaintenanceRunDataManager,
1010
)
11+
from robot_server.runs.run_data_manager import RunDataManager
1112
from opentrons.protocol_engine import ProtocolEngine
1213
from robot_server.deck_configuration.store import DeckConfigurationStore
1314

@@ -28,10 +29,16 @@ def mock_protocol_engine(decoy: Decoy) -> ProtocolEngine:
2829

2930
@pytest.fixture
3031
def mock_maintenance_run_data_manager(decoy: Decoy) -> MaintenanceRunDataManager:
31-
"""Get a mock RunDataManager."""
32+
"""Get a mock MaintenanceRunDataManager."""
3233
return decoy.mock(cls=MaintenanceRunDataManager)
3334

3435

36+
@pytest.fixture
37+
def mock_run_data_manager(decoy: Decoy) -> RunDataManager:
38+
"""Get a mock RunDataManager."""
39+
return decoy.mock(cls=RunDataManager)
40+
41+
3542
@pytest.fixture
3643
def mock_deck_configuration_store(decoy: Decoy) -> DeckConfigurationStore:
3744
"""Get a mock DeckConfigurationStore."""

0 commit comments

Comments
 (0)