# SPDX-FileCopyrightText: 2026 James Harton # # SPDX-License-Identifier: Apache-2.0 defmodule BB.MCP.Resources.RobotTopology do @moduledoc """ Static topology of a robot — links, joints, sensors, and actuators. Read via `bb://robots/{robot}/topology`. The contents do not change after the robot is compiled. """ use Anubis.Server.Component, type: :resource, uri_template: "bb://robots/{robot}/topology", name: "robot_topology" alias Anubis.Server.Response alias BB.MCP.Resources alias BB.Robot alias BB.Robot.Runtime @impl true def description, do: "Links, joints, sensors, and actuators that make up the robot" @impl true def mime_type, do: "application/json" @impl true def read(params, frame) do case Resources.fetch_robot(params) do {:ok, module} -> robot = Runtime.get_robot(module) payload = %{ "name" => to_string(robot.name), "root_link" => to_string(robot.root_link), "links" => robot.links |> Map.keys() |> Enum.map(&to_string/1) |> Enum.sort(), "joints" => Enum.map(Robot.joints_in_order(robot), &format_joint(robot, &1)), "sensors" => robot.sensors |> Map.values() |> Enum.map(&format_sensor/1), "actuators" => robot.actuators |> Map.values() |> Enum.map(&format_actuator/1) } {:reply, Response.json(Response.resource(), payload), frame} {:error, error} -> {:error, error, frame} end end defp format_joint(_robot, joint) do %{ "name" => to_string(joint.name), "type" => to_string(joint.type), "parent_link" => to_string(joint.parent_link), "child_link" => to_string(joint.child_link) } |> maybe_put_limits(joint) end defp maybe_put_limits(map, %{limit: nil}), do: map defp maybe_put_limits(map, %{limit: limit}) when is_map(limit) do Map.put(map, "limit", %{ "lower" => Map.get(limit, :lower), "upper" => Map.get(limit, :upper), "velocity" => Map.get(limit, :velocity), "effort" => Map.get(limit, :effort) }) end defp maybe_put_limits(map, _), do: map defp format_sensor(%{name: name, attached_to: attached_to}) do %{"name" => to_string(name), "attached_to" => format_attached_to(attached_to)} end defp format_actuator(%{name: name, joint: joint}) do %{"name" => to_string(name), "joint" => to_string(joint)} end defp format_attached_to({:link, name}), do: %{"link" => to_string(name)} defp format_attached_to({:joint, name}), do: %{"joint" => to_string(name)} defp format_attached_to(:robot), do: %{"robot" => true} defp format_attached_to(other), do: inspect(other) end