# SPDX-FileCopyrightText: 2025 James Harton # # SPDX-License-Identifier: Apache-2.0 defmodule BB.LiveView.DashboardLive do @moduledoc """ Main LiveView for the BB Dashboard. Displays robot status and control widgets including: - Safety status and controls - Joint positions and sliders - Event stream monitor - Command execution - 3D visualisation - Parameter editor """ use Phoenix.LiveView, layout: {BB.LiveView.Layouts, :app} import BB.LiveView.Components alias BB.LiveView.Components.Command alias BB.LiveView.Components.EventStream alias BB.LiveView.Components.JointControl alias BB.LiveView.Components.Parameters alias BB.LiveView.Components.Safety alias BB.LiveView.Components.Visualisation @joint_state_throttle_ms 33 @impl Phoenix.LiveView def mount(_params, _session, socket) do robot_module = socket.assigns.robot_module socket = socket |> assign(:robot_name, get_robot_name(robot_module)) |> assign(:connected, connected?(socket)) |> assign(:loading, true) |> assign(:pending_joint_state, nil) |> assign(:joint_state_flush_scheduled, false) if connected?(socket) do subscribe_to_robot(robot_module) send(self(), :load_robot_data) end {:ok, socket} end @impl Phoenix.LiveView def handle_info(:load_robot_data, socket) do {:noreply, assign(socket, :loading, false)} end def handle_info({:bb, [:state_machine], %BB.Message{payload: payload} = message}, socket) do new_state = payload.to in_error = new_state == :error armed = new_state in [:armed, :idle, :executing] send_update(Safety, id: "safety", event: {:state_changed, new_state, in_error} ) send_update(JointControl, id: "joint_control", event: {:armed_changed, armed} ) send_update(Command, id: "command", event: {:state_changed, new_state} ) send_update(EventStream, id: "event_stream", event: {:new_message, [:state_machine], message} ) {:noreply, socket} end def handle_info( {:bb, [:sensor | _rest] = path, %BB.Message{payload: %BB.Message.Sensor.JointState{}} = message}, socket ) do socket = socket |> assign(:pending_joint_state, {path, message}) |> schedule_joint_state_flush() {:noreply, socket} end def handle_info(:flush_joint_state, %{assigns: %{pending_joint_state: nil}} = socket) do {:noreply, assign(socket, :joint_state_flush_scheduled, false)} end def handle_info(:flush_joint_state, socket) do {path, message} = socket.assigns.pending_joint_state js = message.payload positions = Enum.zip(js.names, js.positions) |> Map.new() send_update(JointControl, id: "joint_control", event: {:positions_updated, positions} ) send_update(Visualisation, id: "visualisation", event: {:positions_updated, positions} ) send_update(EventStream, id: "event_stream", event: {:new_message, path, message} ) {:noreply, socket |> assign(:pending_joint_state, nil) |> assign(:joint_state_flush_scheduled, false)} end def handle_info( {:bb, [:param | path], %BB.Message{payload: %BB.Parameter.Changed{} = changed} = message}, socket ) do send_update(Parameters, id: "parameters", event: {:parameter_changed, path, changed.new_value} ) send_update(EventStream, id: "event_stream", event: {:new_message, [:param | path], message} ) {:noreply, socket} end def handle_info({:bb, path, %BB.Message{} = message}, socket) do send_update(EventStream, id: "event_stream", event: {:new_message, path, message} ) {:noreply, socket} end # Direct notification from JointControl for immediate visualisation update def handle_info({:joint_position_changed, positions}, socket) do send_update(Visualisation, id: "visualisation", event: {:positions_updated, positions} ) {:noreply, socket} end # Command completion notification from Task def handle_info({:command_complete, component_id, {:ok, result}}, socket) do send_update(Command, id: component_id, event: {:command_result, result} ) {:noreply, socket} end def handle_info({:command_complete, component_id, {:error, reason}}, socket) do send_update(Command, id: component_id, event: {:command_error, reason} ) {:noreply, socket} end def handle_info(_msg, socket) do {:noreply, socket} end @impl Phoenix.LiveView def render(assigns) do ~H"""
Loading robot data...