"""
end
defp render_number_input(assigns) do
{value, unit} = magnitude_and_unit(assigns.param.value, assigns.param.type)
step = if assigns.param.type == "integer", do: "1", else: "0.01"
assigns =
assigns
|> Map.put(:step, step)
|> Map.put(:unit, unit)
|> Map.put(:value, value || 0)
~H"""
"""
end
defp render_atom_input(assigns) do
display_value = if assigns.param.value, do: ":#{assigns.param.value}", else: ""
assigns = Map.put(assigns, :display_value, display_value)
~H"""
"""
end
defp render_text_input(assigns) do
~H"""
"""
end
# LiveView reads `phx-value-*` from the form for change and submit events, not
# from the input that changed, so which parameter a form writes to has to
# travel as form data.
attr(:path, :string, required: true)
attr(:remote, :boolean, required: true)
attr(:bridge, :string, required: true)
defp param_identity(assigns) do
~H"""
"""
end
# A unit-typed parameter can hold any value compatible with its declared unit
# — `BB.Parameter` stores whatever was written rather than converting — so the
# magnitude is only comparable with the declared bounds once it has been
# converted into the declared unit.
defp magnitude_and_unit(nil, _type), do: {nil, nil}
defp magnitude_and_unit(%Localize.Unit{} = value, "unit:" <> declared) do
converted = in_unit(value, BB.Unit.unit_name(declared))
{converted.value, converted.name}
end
defp magnitude_and_unit(%Localize.Unit{} = value, _type), do: {value.value, value.name}
defp magnitude_and_unit(value, _type), do: {value, nil}
defp in_unit(%Localize.Unit{name: name} = value, name), do: value
defp in_unit(value, name) do
case Localize.Unit.convert(value, name) do
{:ok, converted} -> converted
{:error, _reason} -> value
end
end
@impl Phoenix.LiveComponent
def handle_event("select_tab", %{"tab" => tab_str}, socket) do
tab_id = parse_tab_id(tab_str)
{:noreply, assign(socket, :active_tab, tab_id)}
end
def handle_event("toggle_boolean", params, socket) do
path_str = params["path"]
is_remote = params["remote"] == "true"
bridge = params["bridge"]
current_value = Map.get(find_param(socket, path_str, is_remote, bridge), :value)
apply_parameter_change(socket, path_str, not (current_value == true), is_remote, bridge)
end
def handle_event("set_parameter", params, socket) do
path_str = params["path"]
value = params["value"]
is_remote = params["remote"] == "true"
bridge = params["bridge"]
param_type = Map.get(find_param(socket, path_str, is_remote, bridge), :type, "string")
parsed_value = parse_value(value, param_type)
apply_parameter_change(socket, path_str, parsed_value, is_remote, bridge)
end
def handle_event("refresh_remote", %{"bridge" => bridge_str}, socket) do
bridge_atom = String.to_existing_atom(bridge_str)
params = fetch_remote_params(socket.assigns.robot_module, bridge_atom)
tab_id = {:bridge, bridge_atom}
updated_params = Map.put(socket.assigns.parameters, tab_id, params)
{:noreply, assign(socket, :parameters, updated_params)}
end
defp apply_parameter_change(socket, path_str, value, is_remote, bridge) do
result =
if is_remote do
bridge_atom = String.to_existing_atom(bridge)
Parameter.set_remote(socket.assigns.robot_module, bridge_atom, path_str, value)
else
Parameter.set(socket.assigns.robot_module, parse_path(path_str), value)
end
{:noreply, assign(socket, :error_message, refusal(result))}
end
defp refusal(:ok), do: nil
defp refusal({:error, reason}) when is_exception(reason), do: Exception.message(reason)
defp refusal({:error, reason}), do: inspect(reason)
# Remote parameters are keyed in their bridge's tab by the id the bridge gave
# them, which is a string of the remote system's choosing rather than a path.
defp find_param(socket, param_id, true = _is_remote, bridge) do
bridge_atom = String.to_existing_atom(bridge)
socket.assigns.parameters
|> Map.get({:bridge, bridge_atom}, %{})
|> Map.get(param_id, %{})
end
defp find_param(socket, path_str, false = _is_remote, _bridge) do
path = parse_path(path_str)
socket.assigns.parameters
|> Map.get(get_tab_id_for_path(path), %{})
|> Map.get(path, %{})
end
defp get_tab_id_for_path(path) do
case path do
[single] when is_atom(single) -> :general
[group | _] -> group
end
end
# Parameter discovery
defp discover_local_parameters(robot_module) do
params = Parameter.list(robot_module)
organise_into_tabs(params)
end
defp organise_into_tabs(params) do
grouped =
params
|> Enum.group_by(fn {path, _meta} ->
case path do
[single] when is_atom(single) -> :general
[group | _rest] -> group
end
end)
tabs =
grouped
|> Map.keys()
|> Enum.sort_by(fn
:general -> {0, ""}
name -> {1, Atom.to_string(name)}
end)
|> Enum.map(fn group ->
%{
id: group,
label: format_tab_label(group),
type: :local
}
end)
parameters =
grouped
|> Enum.map(fn {group, params_list} ->
formatted =
params_list
|> Enum.map(&format_local_param/1)
|> Enum.sort_by(& &1.display_name)
|> Map.new(fn p -> {p.path, p} end)
{group, formatted}
end)
|> Map.new()
{tabs, parameters}
end
defp format_tab_label(:general), do: "General"
defp format_tab_label(name), do: name |> Atom.to_string() |> String.capitalize()
defp format_local_param({path, meta}) do
display_name =
case path do
[_single] -> Atom.to_string(hd(path))
[_group | rest] -> Enum.map_join(rest, ".", &Atom.to_string/1)
end
%{
path: path,
display_name: display_name,
value: meta[:value],
type: format_type(meta[:type]),
min: meta[:min],
max: meta[:max],
doc: meta[:doc]
}
end
defp format_type(nil), do: "string"
defp format_type(type) when is_atom(type), do: Atom.to_string(type)
defp format_type({:unit, unit}), do: "unit:#{unit}"
defp format_type(other), do: inspect(other)
defp discover_bridge_parameters(robot_module) do
simulation_mode = RobotRuntime.simulation_mode(robot_module)
bridges =
robot_module
|> DslInfo.parameters()
|> Enum.filter(&is_struct(&1, BB.Dsl.Bridge))
|> Enum.reject(fn bridge ->
simulation_mode != nil and bridge.simulation == :omit
end)
tabs =
Enum.map(bridges, fn bridge ->
%{
id: {:bridge, bridge.name},
label: bridge.name |> Atom.to_string() |> String.capitalize(),
type: :remote,
bridge_name: bridge.name
}
end)
parameters =
bridges
|> Enum.map(fn bridge ->
params = fetch_remote_params(robot_module, bridge.name)
{{:bridge, bridge.name}, params}
end)
|> Map.new()
{tabs, parameters}
end
defp fetch_remote_params(robot_module, bridge_name) do
case Parameter.list_remote(robot_module, bridge_name) do
{:ok, params} ->
params
|> Enum.map(fn p ->
id = p[:id] || p["id"]
%{
id: id,
display_name: id,
value: p[:value] || p["value"],
type: format_type(p[:type] || p["type"]),
min: p[:min] || p["min"],
max: p[:max] || p["max"],
doc: p[:doc] || p["doc"]
}
end)
|> Map.new(fn p -> {p.id, p} end)
{:error, _reason} ->
%{__error__: "Failed to load remote parameters"}
end
end
# Tab ID formatting
defp format_tab_id(:general), do: "general"
defp format_tab_id({:bridge, name}), do: "bridge:#{name}"
defp format_tab_id(name) when is_atom(name), do: Atom.to_string(name)
defp parse_tab_id("general"), do: :general
defp parse_tab_id("bridge:" <> name), do: {:bridge, String.to_existing_atom(name)}
defp parse_tab_id(name), do: String.to_existing_atom(name)
defp format_path_string(path) when is_list(path),
do: Enum.map_join(path, ".", &Atom.to_string/1)
defp format_path_string(id), do: to_string(id)
defp parse_path(path_str) do
path_str
|> String.split(".")
|> Enum.map(&String.to_existing_atom/1)
end
# Value parsing
defp parse_value(value, "boolean"), do: value == true or value == "true"
defp parse_value(value, "integer") do
case Integer.parse(to_string(value)) do
{int, _} -> int
:error -> value
end
end
defp parse_value(value, "float"), do: parse_float_value(value)
defp parse_value(value, "unit:" <> declared) do
with magnitude when is_number(magnitude) <- parse_float_value(value),
{:ok, parsed} <- Localize.Unit.new(magnitude, BB.Unit.unit_name(declared)) do
parsed
else
_ -> value
end
end
defp parse_value(value, "atom") do
name = value |> to_string() |> String.trim_leading(":")
try do
String.to_existing_atom(name)
rescue
# An atom the runtime has never heard of can't be a legal value for the
# parameter either, so hand the text on and let the store refuse it.
ArgumentError -> name
end
end
defp parse_value(value, _type), do: value
defp parse_float_value(value) do
case Float.parse(to_string(value)) do
{float, _} -> float
:error -> value
end
end
defp valid_robot?(robot_module) when is_atom(robot_module) do
function_exported?(robot_module, :robot, 0) and
function_exported?(robot_module, :spark_dsl_config, 0)
end
defp valid_robot?(_), do: false
end