Skip to content
Open
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
5 changes: 5 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ spark_locals_without_parens = [
mesh: 1,
min: 1,
name: 1,
offset: 1,
origin: 0,
origin: 1,
param: 1,
Expand All @@ -82,9 +83,11 @@ spark_locals_without_parens = [
pitch: 1,
radius: 1,
red: 1,
reduction: 1,
registry_module: 1,
registry_options: 1,
required: 1,
reversed?: 1,
roll: 1,
scale: 1,
sensor: 2,
Expand All @@ -100,6 +103,8 @@ spark_locals_without_parens = [
timeout: 1,
topology_max_restarts: 1,
topology_max_seconds: 1,
transmission: 0,
transmission: 1,
type: 1,
upper: 1,
velocity: 1,
Expand Down
38 changes: 38 additions & 0 deletions lib/bb/actuator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,44 @@ defmodule BB.Actuator do
alias BB.Message
alias BB.Message.Actuator.Command

# ----------------------------------------------------------------------------
# Transmission access
# ----------------------------------------------------------------------------

@doc """
Returns the joint's resolved transmission, or `nil` if the joint has no
transmission block.

Only valid from inside an actuator callback (i.e. running in the
wrapper's process). The wrapper resolves the transmission at init,
stores it in the process dictionary, and updates it on parameter
changes. Callbacks that publish JointState in joint-space should use
this to unapply the transmission to motor-space readings.

defmodule MyDriver do
use BB.Actuator

def init(opts), do: {:ok, ...}

def handle_info(:tick, state) do
motor_radians = read_hardware(state)
joint_radians =
case BB.Actuator.current_transmission() do
nil -> motor_radians
t -> BB.Transmission.unapply_position(motor_radians, t)
end

publish_joint_state(joint_radians)
{:noreply, state}
end
end

Controllers and other processes outside the wrapper should use
`BB.Transmission.Resolver.resolve_and_subscribe/2` directly.
"""
@spec current_transmission() :: BB.Transmission.t() | nil
def current_transmission, do: Process.get(:bb_transmission)

# ----------------------------------------------------------------------------
# Position Commands
# ----------------------------------------------------------------------------
Expand Down
104 changes: 83 additions & 21 deletions lib/bb/actuator/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ defmodule BB.Actuator.Server do

use GenServer

alias BB.Message
alias BB.Parameter.Changed, as: ParameterChanged
alias BB.Server.ParamResolution
alias BB.Transmission
alias BB.Transmission.Resolver, as: TransmissionResolver

defstruct [
:callback_module,
:resolved_opts,
:raw_opts,
:param_subscriptions,
:transmission,
:transmission_subscriptions,
:joint_name,
:bb,
:user_state
]
Expand All @@ -35,6 +41,9 @@ defmodule BB.Actuator.Server do
resolved_opts: keyword(),
raw_opts: keyword(),
param_subscriptions: %{[atom()] => atom()},
transmission: Transmission.t() | nil,
transmission_subscriptions: %{atom() => [atom()]},
joint_name: atom() | nil,
bb: %{robot: module(), path: [atom()]},
user_state: term()
}
Expand All @@ -58,28 +67,34 @@ defmodule BB.Actuator.Server do
{param_subscriptions, resolved_opts} =
ParamResolution.resolve_and_subscribe(raw_opts, bb.robot)

joint_name = joint_for_actuator(bb)

{transmission, transmission_subscriptions} =
if joint_name do
TransmissionResolver.resolve_and_subscribe(bb.robot, joint_name)
else
{nil, %{}}
end

Process.put(:bb_transmission, transmission)

base = %__MODULE__{
callback_module: callback_module,
resolved_opts: resolved_opts,
raw_opts: raw_opts,
param_subscriptions: param_subscriptions,
transmission: transmission,
transmission_subscriptions: transmission_subscriptions,
joint_name: joint_name,
bb: bb
}

case callback_module.init(resolved_opts) do
{:ok, user_state} ->
{:ok,
%__MODULE__{
callback_module: callback_module,
resolved_opts: resolved_opts,
raw_opts: raw_opts,
param_subscriptions: param_subscriptions,
bb: bb,
user_state: user_state
}}
{:ok, %{base | user_state: user_state}}

{:ok, user_state, timeout_or_continue} ->
{:ok,
%__MODULE__{
callback_module: callback_module,
resolved_opts: resolved_opts,
raw_opts: raw_opts,
param_subscriptions: param_subscriptions,
bb: bb,
user_state: user_state
}, timeout_or_continue}
{:ok, %{base | user_state: user_state}, timeout_or_continue}

{:stop, reason} ->
{:stop, reason}
Expand All @@ -89,8 +104,34 @@ defmodule BB.Actuator.Server do
end
end

defp joint_for_actuator(%{robot: robot_module, path: path}) do
actuator_name = List.last(path)
robot = robot_module.robot()

case Map.get(robot.actuators, actuator_name) do
%{joint: joint_name} -> joint_name
_ -> nil
end
end

@impl GenServer
def handle_info({:bb, [:param | param_path], %{payload: %ParameterChanged{}}}, state) do
state =
case TransmissionResolver.handle_change(
param_path,
state.transmission,
state.transmission_subscriptions,
state.bb.robot,
state.joint_name
) do
{:changed, new_transmission} ->
Process.put(:bb_transmission, new_transmission)
%{state | transmission: new_transmission}

:ignored ->
state
end

case ParamResolution.handle_change(
param_path,
state.param_subscriptions,
Expand All @@ -111,7 +152,14 @@ defmodule BB.Actuator.Server do
end
end

def handle_info(msg, state) do
def handle_info({:bb, topic, %Message{} = message}, state) do
transformed = Transmission.apply_to_command(message, state.transmission)
delegate_handle_info({:bb, topic, transformed}, state)
end

def handle_info(msg, state), do: delegate_handle_info(msg, state)

defp delegate_handle_info(msg, state) do
case state.callback_module.handle_info(msg, state.user_state) do
{:noreply, new_user_state} ->
{:noreply, %{state | user_state: new_user_state}}
Expand All @@ -125,7 +173,14 @@ defmodule BB.Actuator.Server do
end

@impl GenServer
def handle_call(request, from, state) do
def handle_call({:command, %Message{} = message}, from, state) do
transformed = Transmission.apply_to_command(message, state.transmission)
delegate_handle_call({:command, transformed}, from, state)
end

def handle_call(request, from, state), do: delegate_handle_call(request, from, state)

defp delegate_handle_call(request, from, state) do
case state.callback_module.handle_call(request, from, state.user_state) do
{:reply, reply, new_user_state} ->
{:reply, reply, %{state | user_state: new_user_state}}
Expand All @@ -148,7 +203,14 @@ defmodule BB.Actuator.Server do
end

@impl GenServer
def handle_cast(request, state) do
def handle_cast({:command, %Message{} = message}, state) do
transformed = Transmission.apply_to_command(message, state.transmission)
delegate_handle_cast({:command, transformed}, state)
end

def handle_cast(request, state), do: delegate_handle_cast(request, state)

defp delegate_handle_cast(request, state) do
case state.callback_module.handle_cast(request, state.user_state) do
{:noreply, new_user_state} ->
{:noreply, %{state | user_state: new_user_state}}
Expand Down
39 changes: 38 additions & 1 deletion lib/bb/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,42 @@ defmodule BB.Dsl do
]
}

@transmission %Entity{
name: :transmission,
describe: """
A mechanical transmission between the joint and its actuator(s).

Captures the relationship between joint-space command and motor-space
command: gear reduction, zero-offset, and polarity. The URDF equivalent
is `<transmission>`.
""",
target: BB.Dsl.Transmission,
identifier: {:auto, :unique_integer},
imports: [BB.Unit, BB.Dsl.ParamRef],
schema: [
reduction: [
type: {:or, [:float, {:struct, BB.Dsl.ParamRef}]},
doc:
"Gear ratio between actuator and joint. A reduction of `n` means the actuator rotates `n` times for one rotation of the joint. Defaults to `1.0` (direct drive). May be a `param/1` reference.",
required: false,
default: 1.0
],
offset: [
type: {:or, [unit_type(compatible: :degree), unit_type(compatible: :meter)]},
doc:
"Zero-point offset between joint frame and actuator frame: the joint angle (or linear position) corresponding to the actuator's zero. May be a `param/1` reference.",
required: false
],
reversed?: [
type: {:or, [:boolean, {:struct, BB.Dsl.ParamRef}]},
doc:
"Whether actuator motion is reversed relative to joint motion. May be a `param/1` reference.",
required: false,
default: false
]
]
}

@sensor %Entity{
name: :sensor,
describe: "A sensor attached to the robot, a link, or a joint.",
Expand Down Expand Up @@ -281,11 +317,12 @@ defmodule BB.Dsl do
],
dynamics: [@dynamics],
limit: [@limit],
transmission: [@transmission],
sensors: [@sensor],
actuators: [@actuator]
],
recursive_as: :joints,
singleton_entity_keys: [:dynamics, :origin, :axis, :link, :limit],
singleton_entity_keys: [:dynamics, :origin, :axis, :link, :limit, :transmission],
schema: [
name: [
type: :atom,
Expand Down
4 changes: 3 additions & 1 deletion lib/bb/dsl/joint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ defmodule BB.Dsl.Joint do
link: nil,
dynamics: nil,
limit: nil,
transmission: nil,
sensors: [],
actuators: []

alias BB.Dsl.{Actuator, Axis, Dynamics, Limit, Link, Origin, Sensor}
alias BB.Dsl.{Actuator, Axis, Dynamics, Limit, Link, Origin, Sensor, Transmission}
alias Spark.Dsl.Entity

@type t :: %__MODULE__{
Expand All @@ -31,6 +32,7 @@ defmodule BB.Dsl.Joint do
link: Link.t(),
dynamics: nil | Dynamics.t(),
limit: nil | Limit.t(),
transmission: nil | Transmission.t(),
sensors: [Sensor.t()],
actuators: [Actuator.t()]
}
Expand Down
28 changes: 28 additions & 0 deletions lib/bb/dsl/transmission.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: 2025 James Harton
#
# SPDX-License-Identifier: Apache-2.0

defmodule BB.Dsl.Transmission do
@moduledoc """
Mechanical transmission between a joint and its actuator(s).

Captures the relationship between joint-space command and motor-space
command — gear reduction, zero-offset, and polarity. The URDF equivalent
is `<transmission>`.
"""
defstruct __identifier__: nil,
__spark_metadata__: nil,
reduction: 1.0,
offset: nil,
reversed?: false

alias Spark.Dsl.Entity

@type t :: %__MODULE__{
__identifier__: any,
__spark_metadata__: Entity.spark_meta(),
reduction: number,
offset: nil | Localize.Unit.t(),
reversed?: boolean
}
end
Loading
Loading