Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/project-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,14 @@ For workflow use, install or pair the Runtime and open **Inspect a Compute
Device**. Select the computer on `ComputeDevice`; immediately before each editor
cook, Blacknode reads current ROS state through the authenticated Runtime and
`DeviceInspect` exposes the graph inventory, capability candidates, and
unclassified interfaces. Saved workflows contain the stable device ID and
display identity, never live machine state, an SSH password, or a pairing token.
unclassified interfaces. Build an operational stream as `ComputeDevice` →
`PhysicalRobot` → `RobotDeployment` → `RobotStream`. Each node owns one choice,
so the graph can branch from a stable robot or deployment into separate map,
camera, LiDAR, IMU, and future stream paths. Connect the selected stream topic
and message type to a generic ROS 2 node, then connect its message output to the
matching visualization or processing node. Saved workflows contain stable
device, robot, deployment, and topic identity, never live machine state, an SSH
password, or a pairing token.

Installation remains a separate explicit action after the inspection report.
**Install Runtime only** is the default for a new compute device. Before the
Expand Down Expand Up @@ -254,6 +260,10 @@ Connection can be connected, disconnected, checking, unknown, or unreachable;
deployment can be active, inactive, completed, failed, or absent. The latest
inactive deployment remains available on the Runtime and can restart after the
robot passes the same connected, disarmed, calibration, and ownership checks.
Managed capabilities remain visible on the robot card after deployment. Mapping
shows its live occupancy stream while running and keeps restart and saved-map
state available when stopped. Start, save, and stop actions report lifecycle
progress in both the robot card and Deployments.

Update the graph, check setup again, and send a new revision to iterate. Project
artifacts retain evidence from datasets, training runs, policies, evaluations,
Expand Down
144 changes: 127 additions & 17 deletions editor-server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5346,6 +5346,36 @@ def _device_host_live_inspection(host_id: str) -> dict[str, Any]:
if str(value or "").strip()
]
diagnostics_ok = bool(diagnostics.get("ok"))
try:
deployment_payload = client.list_deployments()
except (DeviceRegistryError, AttributeError, TypeError):
deployment_payload = {}
robot_targets = [
{
"id": str(robot.get("id") or ""),
"name": str(robot.get("name") or robot.get("id") or "Robot"),
"remote_device_id": str(robot.get("remote_device_id") or ""),
"paused": bool(robot.get("paused")),
}
for robot in (host.get("robots") or [])
if isinstance(robot, dict) and str(robot.get("id") or "")
]
robot_ids = {robot["id"] for robot in robot_targets}
deployments = []
for value in deployment_payload.get("deployments", []):
if not isinstance(value, dict):
continue
target_id = str(value.get("target_device_id") or "")
if target_id and target_id not in robot_ids:
continue
deployment = _robot_deployment_summary(value)
deployment.update({
"target_device_id": target_id,
"project_id": str(value.get("project_id") or ""),
"workflow_slug": str(value.get("workflow_slug") or ""),
"updated_at": str(value.get("updated_at") or ""),
})
deployments.append(deployment)
inspection = {
"ok": diagnostics_ok,
"live": diagnostics_ok,
Expand Down Expand Up @@ -5395,6 +5425,8 @@ def _device_host_live_inspection(host_id: str) -> dict[str, Any]:
"instances": [],
"suggested_port": 0,
"suggested_instance_id": "",
"robots": robot_targets,
"deployments": deployments,
"ros2_graph": {
"available": bool(diagnostics.get("available")),
"state": "available" if diagnostics_ok else "unavailable",
Expand All @@ -5409,7 +5441,65 @@ def _device_host_live_inspection(host_id: str) -> dict[str, Any]:
"diagnostics_summary": str(diagnostics.get("summary") or ""),
},
}
return _classify_inspected_ros2_graph(inspection)
inspection = _classify_inspected_ros2_graph(inspection)
streams: list[dict[str, Any]] = []
seen_streams: set[tuple[str, str, str]] = set()
graph = inspection.get("ros2_graph")
capabilities = (
graph.get("capabilities")
if isinstance(graph, dict)
and isinstance(graph.get("capabilities"), list)
else []
)
for candidate in capabilities:
if not isinstance(candidate, dict):
continue
capability = str(candidate.get("capability") or "").strip()
for evidence in candidate.get("evidence") or []:
if not isinstance(evidence, dict) or evidence.get("kind") != "topic":
continue
topic = str(evidence.get("name") or "").strip()
message_type = str(evidence.get("message_type") or "").strip()
key = (capability, topic, message_type)
if not capability or not topic or key in seen_streams:
continue
seen_streams.add(key)
streams.append({
"kind": "blacknode.deployed-stream",
"schema_version": 1,
"source": "ros2_graph",
"capability": capability,
"device_id": host_id,
"robot_id": "",
"deployment_id": "",
"state": "available",
"available": True,
"topic": topic,
"message_type": message_type,
})
for deployment in deployments:
if int(deployment.get("mapping_control_count") or 0) != 1:
continue
topic = str(deployment.get("mapping_topic") or "/map")
key = ("map", topic, "nav_msgs/msg/OccupancyGrid")
if key in seen_streams:
continue
seen_streams.add(key)
streams.append({
"kind": "blacknode.deployed-stream",
"schema_version": 1,
"source": "deployment",
"capability": "map",
"device_id": host_id,
"robot_id": str(deployment.get("target_device_id") or ""),
"deployment_id": str(deployment.get("id") or ""),
"state": str(deployment.get("state") or "stopped"),
"available": str(deployment.get("state") or "") == "running",
"topic": topic,
"message_type": "nav_msgs/msg/OccupancyGrid",
})
inspection["streams"] = streams
return inspection


def _refresh_live_compute_device_params() -> None:
Expand Down Expand Up @@ -10071,6 +10161,37 @@ def _set_device_deployment_lease(device_id: str, *, leased: bool) -> None:
)


def _robot_deployment_summary(
deployment: dict[str, Any],
*,
include_motion: bool = True,
) -> dict[str, Any]:
"""Keep robot-card lifecycle fields portable and credential free."""
summary: dict[str, Any] = {
"id": str(deployment.get("id") or ""),
"name": str(
deployment.get("name")
or deployment.get("id")
or "Deployment"
),
"state": str(deployment.get("state") or "stopped"),
}
motion_count = int(deployment.get("motion_control_count") or 0)
if include_motion or motion_count or deployment.get("motion_armed"):
summary["motion_armed"] = bool(deployment.get("motion_armed"))
summary["motion_control_count"] = motion_count
mapping_count = int(deployment.get("mapping_control_count") or 0)
if mapping_count:
summary["mapping_control_count"] = mapping_count
summary["mapping_topic"] = str(
deployment.get("mapping_topic") or "/map"
)
artifact = deployment.get("last_map_artifact")
if isinstance(artifact, dict):
summary["last_map_artifact"] = dict(artifact)
return summary


def _deployment_aware_device_status(device_id: str) -> dict[str, Any]:
"""Report running deployments separately from physical motion ownership."""
client = _paired_device_client(device_id)
Expand Down Expand Up @@ -10137,13 +10258,7 @@ def _deployment_aware_device_status(device_id: str) -> dict[str, Any]:
}
for item in active
]
deployment = {
"id": str(owner.get("id") or ""),
"name": str(owner.get("name") or owner.get("id") or "Deployment"),
"state": str(owner.get("state") or "running"),
"motion_armed": bool(owner.get("motion_armed")),
"motion_control_count": int(owner.get("motion_control_count") or 0),
}
deployment = _robot_deployment_summary(owner)
if hardware_is_leased:
result["deployment_lease"] = deployment
result["armed"] = bool(deployment.get("motion_armed"))
Expand Down Expand Up @@ -10248,15 +10363,10 @@ def _deployment_aware_device_status(device_id: str) -> dict[str, Any]:
if stored:
deployment = stored[0]
result = dict(status)
inactive_deployment = {
"id": str(deployment.get("id") or ""),
"name": str(
deployment.get("name")
or deployment.get("id")
or "Deployment"
),
"state": str(deployment.get("state") or "stopped"),
}
inactive_deployment = _robot_deployment_summary(
deployment,
include_motion=False,
)
# Keep the old field as a compatibility alias for existing clients.
result["inactive_deployment"] = inactive_deployment
result["stored_deployment"] = inactive_deployment
Expand Down
Loading