Skip to content

feat(camera): expose V1 camera signaling commands - #1023

Open
Lash-L wants to merge 1 commit into
mainfrom
codex/v1-camera-commands
Open

Lash-L wants to merge 1 commit into
mainfrom
codex/v1-camera-commands

Conversation

@Lash-L

@Lash-L Lash-L commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

Expose camera preview control and WebRTC signaling through device.v1_properties.camera, using the existing MQTT RPC channel. Cloud connectivity is required. A single channel strategy avoids repeating a failed or timed-out PIN attempt through local/cloud fallback. This is the next camera increment after #980 and #981 and follows the integration approach discussed in #764.

The trait is created during feature discovery only when is_video_monitor_supported is set; the device manager already restricts V1 traits to V1 vacuum products. Public methods hash PIN/pattern digits, select video quality, fetch typed TURN credentials, exchange typed SDP/ICE data and control voice chat. Bare ok responses normalized by the existing decoder and list acknowledgments are accepted; rejected commands raise without echoing response secrets.

Exclude camera PIN hashes and SDP/ICE request data from RPC debug logs, and replace decrypted response/callback payload logs with metadata. Regression tests exercise actual channel requests, successful responses and malformed response logging without changing the wire payloads.

This adds no dependencies or media connection. The caller owns session ordering, bounded polling, ICE deduplication and teardown; each SDP/ICE read makes one request. Preview stop is not scoped to a client ID; the docs cover successful starts and ambiguous start timeouts. The following streaming/session layer can build on this interface. TURN parsing failures redact payload details and suppress parser exception text in displayed tracebacks.

Validation:

  • Camera trait tests: 39 passed, including supported/unsupported discovery, existing MQTT-channel routing, PIN hashing, SDP/ICE exchange, pending replies, rejected commands, redaction and timeout propagation.
  • Full suite on Python 3.14: 2,326 passed, 53 xfailed, 11 deselected; 99 snapshots passed.
  • All pre-commit hooks passed, including Ruff, Mypy and Codespell.
  • No live camera sessions were started during this change.

Copilot AI balanced review requested due to automatic review settings November 10, 2026 02:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@codecov

codecov Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.05660% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
roborock/devices/traits/v1/camera.py 96.36% 1 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
roborock/callbacks.py 98.21% <100.00%> (+3.57%) ⬆️
roborock/devices/rpc/v1_channel.py 89.89% <100.00%> (+1.08%) ⬆️
roborock/devices/traits/v1/__init__.py 91.90% <100.00%> (+0.07%) ⬆️
roborock/protocols/v1_protocol.py 86.47% <100.00%> (+0.96%) ⬆️
tests/devices/rpc/test_v1_channel.py 99.18% <100.00%> (+0.07%) ⬆️
tests/devices/traits/v1/test_camera.py 100.00% <100.00%> (ø)
tests/test_callbacks.py 100.00% <100.00%> (ø)
roborock/devices/traits/v1/camera.py 96.36% <96.36%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Lash-L Lash-L left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated code review by Gemini 4 Argon in Antigravity using our /review (/code-review) skill (.agents/skills/code-review/SKILL.md / AGENTS.md).

Summary

Clean, well-scoped V1 trait implementation that keeps WebRTC session state out of the trait while redacting sensitive camera signaling payloads from debug logs.

  • Layer 1 (Architecture & Boundaries): CameraTrait cleanly accepts only its injected V1RpcChannel, delegates payload encoding/decoding to roborock.protocols.camera_signaling, and satisfies test_trait_boundary_conformance.py.
  • Layer 2 (Data Models & Wire Mapping): Command methods, CameraTurnServer, CameraSdpDescription, and CameraIceCandidate wire shapes match the V1 camera signaling contract, with two optional command parameter notes inline (stop_camera_preview and start_voice_chat).
  • Layer 3 (Code Style & Conventions): Modern typing (X | None), docstrings, and exception chaining (from None on TURN parse failures to avoid leaking raw credentials) are consistent with repo standards.
  • Layer 4 (Error Handling): _send_ack strictly validates {} / ["ok"] and avoids local/cloud fallback retries on rejected PIN checks.
  • Layer 5 (Testing Standards): Thorough unit tests in tests/devices/traits/v1/test_camera.py and log-redaction tests in tests/devices/rpc/test_v1_channel.py; one small test addition noted inline to cover the uninitialized rpc_channel guard.

"""Return the channel supplied by the device."""
if self._rpc_channel is None:
raise RoborockException("Camera trait has no RPC channel")
return self._rpc_channel

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layer 5 (Testing / Patch Coverage): This Camera trait has no RPC channel guard is the only uncovered line in the PR diff. Adding a unit test in tests/devices/traits/v1/test_camera.py that calls a command on a bare CameraTrait() instance before _rpc_channel is attached will exercise this branch and bring patch coverage to 100%.

async def stop_preview(self) -> None:
"""Stop the active preview; the command is not scoped to a client ID."""
await self._send_ack(RoborockCommand.STOP_CAMERA_PREVIEW)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the wire, stop_camera_preview accepts {"client_id": client_id} matching the client_id passed to start_camera_preview. Consider accepting an optional client_id: str | None = None here (sending {"client_id": client_id} when provided, or None when omitted) so callers can scope teardown to their session ID.

async def start_voice_chat(self) -> None:
"""Enable the robot's microphone for the active WebRTC session."""
await self._send_ack(RoborockCommand.START_VOICE_CHAT)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the wire, start_voice_chat accepts {"play": True, "record": True} (and stop_voice_chat sends {}). Consider accepting optional play: bool | None = None and record: bool | None = None parameters (or forwarding them when specified) so callers can explicitly set the playback and recording flags.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants