Skip to content

Commit a41d7c9

Browse files
committed
fix: resolve credo and dialyzer warnings
Extract helper functions to reduce nesting depth in calibrate, setup_servos, and controller modules. Use catch-all error patterns in command handlers to satisfy dialyzer (can't trace through `:telemetry.span/3`). Fix nested module aliases in Phoenix boilerplate.
1 parent 3cda4af commit a41d7c9

10 files changed

Lines changed: 135 additions & 108 deletions

File tree

lib/bb/example/so101/command/demo_circle.ex

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ defmodule BB.Example.SO101.Command.DemoCircle do
3737
@default_points 16
3838
@default_delay 150
3939

40+
# Motion.move_to/4 can return errors at runtime but dialyzer can't see
41+
# through :telemetry.span/3 and thinks it always returns {:ok, meta}
42+
@dialyzer {:no_match, [handle_command: 3, execute_path: 4]}
43+
4044
@impl BB.Command
4145
def handle_command(goal, context, state) do
4246
radius = Map.get(goal, :radius, @default_radius)
@@ -66,8 +70,8 @@ defmodule BB.Example.SO101.Command.DemoCircle do
6670
{:stop, :normal, %{state | result: {:error, reason}}}
6771
end
6872

69-
{:error, reason, _meta} ->
70-
{:stop, :normal, %{state | result: {:error, {:failed_to_reach_start, reason}}}}
73+
error ->
74+
{:stop, :normal, %{state | result: {:error, {:failed_to_reach_start, error}}}}
7175
end
7276
end
7377

@@ -92,8 +96,8 @@ defmodule BB.Example.SO101.Command.DemoCircle do
9296
Process.sleep(delay)
9397
{:cont, :ok}
9498

95-
{:error, reason, _meta} ->
96-
{:halt, {:error, {:ik_failed, target, reason}}}
99+
error ->
100+
{:halt, {:error, {:ik_failed, target, error}}}
97101
end
98102
end)
99103
end

lib/bb/example/so101/command/move_to_pose.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ defmodule BB.Example.SO101.Command.MoveToPose do
2222
alias BB.IK.DLS.Motion
2323
alias BB.Math.Vec3
2424

25+
# Motion.move_to/4 can return errors at runtime but dialyzer can't see
26+
# through :telemetry.span/3 and thinks it always returns {:ok, meta}
27+
@dialyzer {:no_match, handle_command: 3}
28+
2529
@impl BB.Command
2630
def handle_command(%{target: %Vec3{} = target}, context, state) do
2731
ik_opts = [delivery: :direct, exclude_joints: [:gripper]]
@@ -30,8 +34,8 @@ defmodule BB.Example.SO101.Command.MoveToPose do
3034
{:ok, _meta} ->
3135
{:stop, :normal, %{state | result: :reached}}
3236

33-
{:error, reason, _meta} ->
34-
{:stop, :normal, %{state | result: {:error, {:ik_failed, reason}}}}
37+
error ->
38+
{:stop, :normal, %{state | result: {:error, {:ik_failed, error}}}}
3539
end
3640
end
3741

lib/bb_example_so101/application.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ defmodule BB.Example.SO101.Application do
99

1010
use Application
1111

12+
alias BB.Example.SO101Web
13+
1214
@impl true
1315
def start(_type, _args) do
1416
children =
@@ -30,7 +32,7 @@ defmodule BB.Example.SO101.Application do
3032
# whenever the application is updated.
3133
@impl true
3234
def config_change(changed, _new, removed) do
33-
BB.Example.SO101Web.Endpoint.config_change(changed, removed)
35+
SO101Web.Endpoint.config_change(changed, removed)
3436
:ok
3537
end
3638

lib/bb_example_so101_web.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ defmodule BB.Example.SO101Web do
9292
import BB.Example.SO101Web.CoreComponents
9393

9494
# Common modules used in templates
95-
alias Phoenix.LiveView.JS
9695
alias BB.Example.SO101Web.Layouts
96+
alias Phoenix.LiveView.JS
9797

9898
# Routes generation with the ~p sigil
9999
unquote(verified_routes())
@@ -103,9 +103,9 @@ defmodule BB.Example.SO101Web do
103103
def verified_routes do
104104
quote do
105105
use Phoenix.VerifiedRoutes,
106-
endpoint: BB.Example.SO101Web.Endpoint,
107-
router: BB.Example.SO101Web.Router,
108-
statics: BB.Example.SO101Web.static_paths()
106+
endpoint: unquote(Module.concat(__MODULE__, Endpoint)),
107+
router: unquote(Module.concat(__MODULE__, Router)),
108+
statics: unquote(__MODULE__).static_paths()
109109
end
110110
end
111111

lib/bb_example_so101_web/components/core_components.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ defmodule BB.Example.SO101Web.CoreComponents do
3333
use Phoenix.Component
3434
use Gettext, backend: BB.Example.SO101Web.Gettext
3535

36+
alias Phoenix.HTML.Form
3637
alias Phoenix.LiveView.JS
3738

3839
@doc """
@@ -205,7 +206,7 @@ defmodule BB.Example.SO101Web.CoreComponents do
205206
def input(%{type: "checkbox"} = assigns) do
206207
assigns =
207208
assign_new(assigns, :checked, fn ->
208-
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
209+
Form.normalize_value("checkbox", assigns[:value])
209210
end)
210211

211212
~H"""

lib/bb_example_so101_web/endpoint.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
defmodule BB.Example.SO101Web.Endpoint do
66
use Phoenix.Endpoint, otp_app: :bb_example_so101
77

8+
alias BB.Example.SO101Web
9+
810
# The session will be stored in the cookie and signed,
911
# this means its contents can be read but not tampered with.
1012
# Set :encryption_salt if you would also like to encrypt it.
@@ -28,7 +30,7 @@ defmodule BB.Example.SO101Web.Endpoint do
2830
at: "/",
2931
from: :bb_example_so101,
3032
gzip: not code_reloading?,
31-
only: BB.Example.SO101Web.static_paths(),
33+
only: SO101Web.static_paths(),
3234
raise_on_missing_only: code_reloading?
3335

3436
plug Plug.Static,
@@ -63,5 +65,5 @@ defmodule BB.Example.SO101Web.Endpoint do
6365
plug Plug.MethodOverride
6466
plug Plug.Head
6567
plug Plug.Session, @session_options
66-
plug BB.Example.SO101Web.Router
68+
plug SO101Web.Router
6769
end

lib/mix/tasks/so101.calibrate.ex

Lines changed: 80 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -123,22 +123,14 @@ defmodule Mix.Tasks.So101.Calibrate do
123123
end
124124

125125
defp run_calibration(pid, dry_run) do
126-
# Verify all servos are present
127126
{found, missing} = check_servos(pid)
128127

129128
if missing != [] do
130129
Mix.shell().error("Missing servos: #{inspect(Enum.map(missing, fn {_, id, _} -> id end))}")
131-
Mix.shell().info("Continue anyway? (y/n)")
132130

133-
case IO.gets("") do
134-
data when is_binary(data) ->
135-
if String.trim(data) |> String.downcase() != "y" do
136-
Mix.shell().info("Calibration cancelled.")
137-
return_early()
138-
end
139-
140-
_ ->
141-
return_early()
131+
unless confirm?("Continue anyway?") do
132+
Mix.shell().info("Calibration cancelled.")
133+
return_early()
142134
end
143135
end
144136

@@ -151,21 +143,31 @@ defmodule Mix.Tasks.So101.Calibrate do
151143
"Found #{length(found)} servo(s). Press Enter to disable torque and begin..."
152144
)
153145

154-
case IO.gets("") do
155-
data when is_binary(data) ->
156-
if String.trim(data) |> String.downcase() == "q" do
157-
Mix.shell().info("Calibration cancelled.")
158-
else
159-
do_calibration(pid, found, dry_run)
160-
end
161-
162-
_ ->
163-
Mix.shell().info("Calibration cancelled.")
146+
if prompt_quit?() do
147+
Mix.shell().info("Calibration cancelled.")
148+
else
149+
do_calibration(pid, found, dry_run)
164150
end
165151
end
166152

167153
defp return_early, do: :ok
168154

155+
defp confirm?(prompt) do
156+
Mix.shell().info(prompt <> " (y/n)")
157+
158+
case IO.gets("") do
159+
data when is_binary(data) -> String.trim(data) |> String.downcase() == "y"
160+
_ -> false
161+
end
162+
end
163+
164+
defp prompt_quit? do
165+
case IO.gets("") do
166+
data when is_binary(data) -> String.trim(data) |> String.downcase() == "q"
167+
_ -> true
168+
end
169+
end
170+
169171
defp check_servos(pid) do
170172
Enum.split_with(@joints, fn {_name, servo_id, _desc} ->
171173
case Feetech.ping(pid, servo_id) do
@@ -258,24 +260,7 @@ defmodule Mix.Tasks.So101.Calibrate do
258260
# Read all positions
259261
new_state =
260262
Enum.reduce(joints, state, fn {_name, servo_id, _desc}, acc ->
261-
case Feetech.read_raw(pid, servo_id, :present_position) do
262-
{:ok, raw_pos} ->
263-
update_in(acc, [servo_id], fn data ->
264-
# Unwrap position to handle 0/4095 boundary crossing
265-
unwrapped = unwrap_position(raw_pos, data.raw, data.unwrapped)
266-
267-
%{
268-
data
269-
| raw: raw_pos,
270-
unwrapped: unwrapped,
271-
min_unwrapped: min(data.min_unwrapped, unwrapped),
272-
max_unwrapped: max(data.max_unwrapped, unwrapped)
273-
}
274-
end)
275-
276-
_ ->
277-
acc
278-
end
263+
update_servo_tracking(pid, servo_id, acc)
279264
end)
280265

281266
# Display current state
@@ -285,6 +270,26 @@ defmodule Mix.Tasks.So101.Calibrate do
285270
end
286271
end
287272

273+
defp update_servo_tracking(pid, servo_id, state) do
274+
case Feetech.read_raw(pid, servo_id, :present_position) do
275+
{:ok, raw_pos} ->
276+
update_in(state, [servo_id], fn data ->
277+
unwrapped = unwrap_position(raw_pos, data.raw, data.unwrapped)
278+
279+
%{
280+
data
281+
| raw: raw_pos,
282+
unwrapped: unwrapped,
283+
min_unwrapped: min(data.min_unwrapped, unwrapped),
284+
max_unwrapped: max(data.max_unwrapped, unwrapped)
285+
}
286+
end)
287+
288+
_ ->
289+
state
290+
end
291+
end
292+
288293
# Handle position wraparound at 0/4095 boundary
289294
defp unwrap_position(current_raw, last_raw, last_unwrapped) do
290295
delta = current_raw - last_raw
@@ -350,42 +355,47 @@ defmodule Mix.Tasks.So101.Calibrate do
350355
results =
351356
for {name, servo_id, _desc} <- joints do
352357
data = state[servo_id]
353-
range = data.max_unwrapped - data.min_unwrapped
354-
355-
if range > 10 do
356-
# Calculate center in unwrapped space, then convert to raw (0-4095)
357-
center_unwrapped = div(data.min_unwrapped + data.max_unwrapped, 2)
358-
center_raw = Integer.mod(center_unwrapped, @steps_per_revolution)
359-
360-
# Firmware applies: Present_Position = Actual_Position - Offset
361-
# So: 2048 = center_raw - offset, therefore offset = center_raw - 2048
362-
# Clamp to ±2047 (sign_magnitude bit 11 limit).
363-
offset = center_raw - @center_position
364-
offset = max(-@max_offset_magnitude, min(@max_offset_magnitude, offset))
365-
366-
Mix.shell().info("""
367-
#{format_joint(name)} (ID #{servo_id}):
368-
Range: #{range} steps (#{format_degrees(steps_to_degrees(range))})
369-
Center: #{center_raw} -> Offset: #{offset}
370-
""")
371-
372-
if dry_run do
373-
{name, servo_id, {:ok, %{range: range, center: center_raw, offset: offset}}}
374-
else
375-
case apply_calibration(pid, servo_id, offset) do
376-
:ok -> {name, servo_id, {:ok, %{offset: offset}}}
377-
{:error, reason} -> {name, servo_id, {:error, reason}}
378-
end
379-
end
380-
else
381-
Mix.shell().info(" #{format_joint(name)} (ID #{servo_id}): Skipped (not moved enough)")
382-
{name, servo_id, {:error, :not_moved}}
383-
end
358+
process_joint_result(pid, name, servo_id, data, dry_run)
384359
end
385360

386361
print_summary(results, dry_run)
387362
end
388363

364+
defp process_joint_result(_pid, name, servo_id, data, _dry_run)
365+
when data.max_unwrapped - data.min_unwrapped <= 10 do
366+
Mix.shell().info(" #{format_joint(name)} (ID #{servo_id}): Skipped (not moved enough)")
367+
{name, servo_id, {:error, :not_moved}}
368+
end
369+
370+
defp process_joint_result(pid, name, servo_id, data, dry_run) do
371+
range = data.max_unwrapped - data.min_unwrapped
372+
373+
# Calculate center in unwrapped space, then convert to raw (0-4095)
374+
center_unwrapped = div(data.min_unwrapped + data.max_unwrapped, 2)
375+
center_raw = Integer.mod(center_unwrapped, @steps_per_revolution)
376+
377+
# Firmware applies: Present_Position = Actual_Position - Offset
378+
# So: 2048 = center_raw - offset, therefore offset = center_raw - 2048
379+
# Clamp to ±2047 (sign_magnitude bit 11 limit).
380+
offset = center_raw - @center_position
381+
offset = max(-@max_offset_magnitude, min(@max_offset_magnitude, offset))
382+
383+
Mix.shell().info("""
384+
#{format_joint(name)} (ID #{servo_id}):
385+
Range: #{range} steps (#{format_degrees(steps_to_degrees(range))})
386+
Center: #{center_raw} -> Offset: #{offset}
387+
""")
388+
389+
if dry_run do
390+
{name, servo_id, {:ok, %{range: range, center: center_raw, offset: offset}}}
391+
else
392+
case apply_calibration(pid, servo_id, offset) do
393+
:ok -> {name, servo_id, {:ok, %{offset: offset}}}
394+
{:error, reason} -> {name, servo_id, {:error, reason}}
395+
end
396+
end
397+
end
398+
389399
defp reset_position_offset(pid, servo_id) do
390400
unlock_eeprom(pid, servo_id)
391401

lib/mix/tasks/so101.setup_servos.ex

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -354,33 +354,35 @@ defmodule Mix.Tasks.So101.SetupServos do
354354
════════════════════════════════════════════════════════════════
355355
""")
356356

357-
configured = Enum.count(results, fn {_, _, r} -> r in [:configured, :already_configured] end)
358-
skipped = Enum.count(results, fn {_, _, r} -> r == :skipped end)
359-
failed = Enum.count(results, fn {_, _, r} -> r == :failed end)
360-
cancelled = Enum.count(results, fn {_, _, r} -> r == :cancelled end)
361-
362357
for {joint, id, result} <- results do
363-
status =
364-
case result do
365-
:configured -> "✓ Configured"
366-
:already_configured -> "✓ Already correct"
367-
:skipped -> "○ Skipped"
368-
:failed -> "✗ Failed"
369-
:cancelled -> "○ Cancelled"
370-
end
371-
372-
Mix.shell().info(" #{format_joint(joint)} (ID #{id}): #{status}")
358+
Mix.shell().info(" #{format_joint(joint)} (ID #{id}): #{format_result(result)}")
373359
end
374360

375361
Mix.shell().info("")
376362

377-
cond do
378-
cancelled > 0 ->
379-
Mix.shell().info("Setup was cancelled.")
363+
counts = Enum.frequencies_by(results, fn {_, _, r} -> r end)
364+
print_summary_message(counts)
365+
end
380366

381-
failed > 0 ->
382-
Mix.shell().error("#{failed} servo(s) failed to configure. Please retry those joints.")
367+
defp format_result(:configured), do: "✓ Configured"
368+
defp format_result(:already_configured), do: "✓ Already correct"
369+
defp format_result(:skipped), do: "○ Skipped"
370+
defp format_result(:failed), do: "✗ Failed"
371+
defp format_result(:cancelled), do: "○ Cancelled"
383372

373+
defp print_summary_message(%{cancelled: n}) when n > 0 do
374+
Mix.shell().info("Setup was cancelled.")
375+
end
376+
377+
defp print_summary_message(%{failed: n}) when n > 0 do
378+
Mix.shell().error("#{n} servo(s) failed to configure. Please retry those joints.")
379+
end
380+
381+
defp print_summary_message(counts) do
382+
configured = Map.get(counts, :configured, 0) + Map.get(counts, :already_configured, 0)
383+
skipped = Map.get(counts, :skipped, 0)
384+
385+
cond do
384386
skipped > 0 and configured > 0 ->
385387
Mix.shell().info("#{configured} servo(s) configured successfully, #{skipped} skipped.")
386388

0 commit comments

Comments
 (0)