From 1be412da2bcdc6214c99920f17a71df34fda83ee Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Fri, 21 Aug 2026 15:50:09 +1000 Subject: [PATCH 1/2] feat(swift): support teach() teach panel for Swift-backed robots robot.teach() only ever worked for PyPlot/PyPlot2 -- Swift's connector wrapper explicitly set supports_teach=False, so any hasgeometry=True robot (the default backend Swift resolves to, matching plot()) raised a clean TypeError. examples/teach_swift.py was RTB's own hand-rolled template for what a Swift teach panel looks like, but predates Swift 2.0's AssemblyHandle refactor and still drove the robot via the now- deprecated robot.q[j] = value direct-mutation style. Implements Swift._add_teach_panel() using the current idiomatic pattern (named sliders -> env.values -> one per-step handle.callback, see examples/panda_ik_sliders.py): one slider per joint (degrees for revolute, native units for prismatic -- teach_swift.py's always- degrees conversion was a bug, not carried forward), plus a live end-effector pose readout (6 compact Labels, matching PyPlot's own six fig.text() calls -- needs swift-sim with Label(compact=True), jhavl/ swift#131). Also fixes two things this surfaced, both blocking without it: - teach()'s env.launch("Teach " + self.name, limits=limits) passed the name positionally, which lands on Swift's real launch(realtime=..., ...) rather than PyPlot's launch(name=..., ...) it was written against -- raises ValueError immediately. Fixed to name= as a keyword. - Swift's hold() (what teach()'s shared `if block: env.hold()` relies on to keep the panel open) only sleeps and polls for a disconnect -- it never calls step(), so nothing would ever process a dragged slider. _add_teach_panel() now runs its own env.run() loop instead when block=True, matching every other interactive Swift script's own step() loop, and signals back to teach() so it skips the now- redundant (and occasionally hang-prone -- see below) env.hold() call. Needs swift-sim with the disconnect-during-step fix (jhavl/swift#132) for a closed tab to end that loop gracefully rather than an uncaught TimeoutError; needs #131 too, since headless mode's hold() never reports "disconnected" at all -- teach()'s subsequent env.hold() call would otherwise hang indefinitely, not just waste time. teach()'s docstring now also documents that robot.q holds the final taught pose once teach() returns, true for every backend -- PyPlot achieves this by mutating robot.q throughout its own session; Swift's _add_teach_panel() writes handle.q back once, at the point the session ends, since AssemblyHandle deliberately never mirrors it during the session itself (jhavl/swift#85). A one-time, deliberate exception to "stateless robot model" for this specific single-owner interactive session, not a general precedent -- the same tension PyPlot's own teach() already has (desiderata.md), just accepted here rather than solved. Also: rtb.models.Panda().teach() with no explicit backend= now opens Swift by default (hasgeometry=True resolves there, matching plot()'s existing default) rather than always falling back to PyPlot, since Swift now actually supports it -- a real, intentional behaviour change. --- examples/teach_swift.py | 146 +++++++++++------- src/roboticstoolbox/backends/PyPlot/PyPlot.py | 9 +- .../backends/PyPlot/PyPlot2.py | 11 +- .../backends/swift/__init__.py | 134 +++++++++++++++- src/roboticstoolbox/robot/BaseRobot.py | 49 ++++-- tests/test_BaseRobot.py | 74 +++++++++ tests/test_backend_capabilities.py | 10 +- 7 files changed, 358 insertions(+), 75 deletions(-) diff --git a/examples/teach_swift.py b/examples/teach_swift.py index a75b45e36..4fdb47c71 100644 --- a/examples/teach_swift.py +++ b/examples/teach_swift.py @@ -1,58 +1,96 @@ -# #!/usr/bin/env python -# """ -# @author Jesse Haviland -# """ +#!/usr/bin/env python +""" +Hand-rolled Swift teach panel: one named slider per joint, driving the +robot via a single per-step callback on the handle env.add_robot() +returns -- the same mechanism roboticstoolbox's own robot.teach(q, +backend="swift") now uses internally (see roboticstoolbox.backends.swift. +Swift._add_teach_panel). Useful as a template for a custom panel beyond +what teach() offers (e.g. extra UI elements alongside the sliders); +for the common case, robot.teach(q, backend="swift") does this in one +call. -import swift -import roboticstoolbox as rtb +Named sliders push their value into env.values; the handle's callback +reads from there and returns the new q each step -- there's no explicit +per-slider setter function, and no direct robot.q/handle.q mutation in +the loop, env.step() drives everything. +""" import numpy as np -import time +import roboticstoolbox as rtb +from swift import Swift, Slider, Label # Launch the simulator Swift -env = swift.Swift() -env.launch() - -# Make a Panda robot and add it to Swift -panda = rtb.models.UR5() -panda.q = panda.qr -env.add(panda) - - -# This is our callback funciton from the sliders in Swift which set -# the joint angles of our robot to the value of the sliders -def set_joint(j, value): - panda.q[j] = np.deg2rad(float(value)) - - -# Loop through each link in the Panda and if it is a variable joint, -# add a slider to Swift to control it -j = 0 -for link in panda.links: - if link.isjoint: - # We use a lambda as the callback function from Swift - # j=j is used to set the value of j rather than the variable j - # We use the HTML unicode format for the degree sign in the unit arg - env.add( - swift.Slider( - lambda x, j=j: set_joint(j, x), - min=np.round(np.rad2deg(link.qlim[0]), 2), - max=np.round(np.rad2deg(link.qlim[1]), 2), - step=1, - value=np.round(np.rad2deg(panda.q[j]), 2), - desc="Panda Joint " + str(j), - unit="°", - ) - ) - - j += 1 - - -while True: - # Process the event queue from Swift, this invokes the callback functions - # from the sliders if the slider value was changed - # env.process_events() - - # Update the environment with the new robot pose - env.step(0) - - time.sleep(0.01) +env = Swift() +env.launch(ground_opacity=0.3) + +# Make a robot and add it to Swift +# robot = rtb.models.UR5() +robot = rtb.models.Panda() + +handle = env.add_robot(robot) +handle.q = robot.qr + +# compact=True keeps six stacked Labels from taking up excessive +# sidebar space -- Label's default styling is sized for an occasional +# standalone heading, not several stacked close together. +pose_labels = [Label("", compact=True) for _ in range(6)] +for label in pose_labels: + env.add(label) + + +def update_pose_labels(q): + T = robot.fkine(q) + t = np.round(T.t, 3) + r = np.round(T.rpy(unit="deg"), 3) + pose_labels[0].desc = f"x: {t[0]}" + pose_labels[1].desc = f"y: {t[1]}" + pose_labels[2].desc = f"z: {t[2]}" + pose_labels[3].desc = f"r: {r[0]}°" + pose_labels[4].desc = f"p: {r[1]}°" + pose_labels[5].desc = f"y: {r[2]}°" + + +def teach_update(t, values): + # Sliders display revolute joints in degrees, prismatic in native + # units (metres) -- toradians() converts the whole vector back in + # one call, only touching revolute entries. + q_display = np.array([values[f"q{j}"] for j in range(robot.n)]) + q_new = robot.toradians(q_display) + update_pose_labels(q_new) + return q_new + + +handle.callback = teach_update + +# Loop through each joint and add a slider to Swift to control it +qlim = robot.qlim +for j in range(robot.n): + lo, hi = qlim[0, j], qlim[1, j] + if robot.isrevolute(j): + lo_disp, hi_disp, val_disp = np.degrees(lo), np.degrees(hi), np.degrees(handle.q[j]) + step = 1.0 + unit = "°" + else: + lo_disp, hi_disp, val_disp = lo, hi, handle.q[j] + step = (hi - lo) / 100 + unit = "m" + + env.add( + Slider( + lambda x: None, + # min/max/value stay full precision -- precision= below only + # rounds the *displayed* text, so the slider's actual driven + # value doesn't lose precision to display rounding. + min=float(lo_disp), + max=float(hi_disp), + step=step, + value=float(val_disp), + desc=f"{robot.name} joint {j}", + unit=unit, + precision=2, + ), + name=f"q{j}", + ) + +update_pose_labels(handle.q) + +env.run() diff --git a/src/roboticstoolbox/backends/PyPlot/PyPlot.py b/src/roboticstoolbox/backends/PyPlot/PyPlot.py index 3ceabe913..3d4f042ed 100644 --- a/src/roboticstoolbox/backends/PyPlot/PyPlot.py +++ b/src/roboticstoolbox/backends/PyPlot/PyPlot.py @@ -586,7 +586,14 @@ def _set_axes_equal(self): self.ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) self.ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) - def _add_teach_panel(self, robot, q): + def _add_teach_panel(self, robot, q, handle=None, block=True): + # handle, block: unused here -- PyPlot has no AssemblyHandle + # concept (it drives the panel by mutating robot.q directly + # below), and matplotlib's own GUI mainloop (entered via this + # backend's env.hold()) already processes slider events on its + # own, unlike Swift's hold() which needs an active step() loop. + # Both params only meaningful for the Swift backend's own + # _add_teach_panel(). if _isnotebook(): raise RuntimeError("cannot use teach panel under Jupyter") diff --git a/src/roboticstoolbox/backends/PyPlot/PyPlot2.py b/src/roboticstoolbox/backends/PyPlot/PyPlot2.py index bec37b6e3..6b2b703cc 100644 --- a/src/roboticstoolbox/backends/PyPlot/PyPlot2.py +++ b/src/roboticstoolbox/backends/PyPlot/PyPlot2.py @@ -357,7 +357,7 @@ def _push_inline_frame(self): # def _plot_handler(self, sig, frame): # plt.pause(0.001) - def _add_teach_panel(self, robot, q): + def _add_teach_panel(self, robot, q, handle=None, block=True): """ Add a teach panel @@ -365,6 +365,15 @@ def _add_teach_panel(self, robot, q): :type robot: ERobot class :param q: inital joint angles in radians :type q: array_like(n) + :param handle: unused here -- PyPlot2 has no AssemblyHandle + concept, it drives the panel by mutating robot.q directly + below. Only meaningful for the Swift backend's own + _add_teach_panel(). + :param block: unused here -- matplotlib's own GUI mainloop + (entered via this backend's env.hold()) already processes + slider events on its own, unlike Swift's hold() which needs + an active step() loop. Only meaningful for the Swift + backend's own _add_teach_panel(). """ fig = self.fig diff --git a/src/roboticstoolbox/backends/swift/__init__.py b/src/roboticstoolbox/backends/swift/__init__.py index a75ca61db..817134ee9 100644 --- a/src/roboticstoolbox/backends/swift/__init__.py +++ b/src/roboticstoolbox/backends/swift/__init__.py @@ -1,3 +1,5 @@ +import numpy as np + from swift.SwiftRoute import SwiftServer, SwiftSocket, start_servers from swift.SwiftElement import ( SwiftElement, @@ -14,9 +16,139 @@ class Swift(_SwiftBase): """Swift backend with RTB capability flags.""" - supports_teach: bool = False + supports_teach: bool = True supports_ellipse: bool = False + def _add_teach_panel(self, robot, q, handle, block): + """ + Add a joint-slider teach panel plus a live end-effector pose + readout, and wire the sliders to drive ``handle`` (the + AssemblyHandle ``BaseRobot.teach()`` got back from ``self.add()``) + via a single per-step callback -- see jhavl/swift#85 for why + driving the robot model's own .q/.qd directly is deprecated. + + :param robot: the robot being taught, already added to this scene + :param q: initial joint configuration to seed the panel/display + with -- may differ from ``robot.q``'s current value + :param handle: the AssemblyHandle ``self.add(robot, readonly=True)`` + returned for this robot instance + :param block: unlike PyPlot (whose own env.hold() enters + matplotlib's GUI mainloop, which processes slider events on + its own), Swift's hold() only sleeps and polls for a + disconnect -- it never calls step(), so nothing would ever + notice a dragged slider without an active step() loop + running somewhere. When block, this method runs that loop + itself (self.run(), blocking until disconnect/^C) rather + than relying on teach()'s own later `if block: env.hold()`. + When not block, a single self.step() seeds the initial + display and the caller is responsible for stepping (matches + teach(block=False)'s existing contract for other backends). + :returns: True if block, signalling to teach()'s shared code + that this method already fully handled blocking itself -- + teach()'s own `if block: env.hold()` must be skipped in that + case, not just as an optimisation: calling env.hold() a + second time here can hang outright (its disconnect-poll + never expires in headless mode, and even non-headless + there's a race around close() and socket.USERS). None + (falsy) when not block, matching PyPlot/PyPlot2's own + _add_teach_panel, which never blocks internally at all. + """ + qlim = robot.qlim + + # One label per value (x/y/z/r/p/y), matching PyPlot's own six + # separate fig.text() calls -- compact=True keeps this from + # taking up excessive sidebar space (Label's default styling is + # sized for an occasional standalone heading, not several + # stacked close together -- see swift's Label(compact=) docstring). + pose_labels = [Label("", compact=True) for _ in range(6)] + for label in pose_labels: + self.add(label) + + def update_pose_labels(qv): + T = robot.fkine(qv) + t = np.round(T.t, 3) + r = np.round(T.rpy(unit="deg"), 3) + pose_labels[0].desc = f"x: {t[0]}" + pose_labels[1].desc = f"y: {t[1]}" + pose_labels[2].desc = f"z: {t[2]}" + pose_labels[3].desc = f"r: {r[0]}°" + pose_labels[4].desc = f"p: {r[1]}°" + pose_labels[5].desc = f"y: {r[2]}°" + + def teach_update(t, values): + # Sliders display revolute joints in degrees, prismatic in + # native units (metres) -- toradians() converts the whole + # vector back in one call, only touching revolute entries. + q_display = np.array([values[f"q{j}"] for j in range(robot.n)]) + q_new = robot.toradians(q_display) + update_pose_labels(q_new) + return q_new + + # Safe with readonly=True: Swift.step()'s callback branch runs + # unconditionally whenever a callback is set -- readonly only + # gates the *other* (non-callback) per-step update path. + handle.callback = teach_update + + for j in range(robot.n): + lo, hi = qlim[0, j], qlim[1, j] + if robot.isrevolute(j): + lo_disp, hi_disp, val_disp = np.degrees(lo), np.degrees(hi), np.degrees(q[j]) + step = 1.0 + unit = "°" + else: + lo_disp, hi_disp, val_disp = lo, hi, q[j] + step = (hi - lo) / 100 + unit = "m" + + self.add( + Slider( + lambda x: None, + # min/max/value stay full precision -- precision= + # below only rounds the *displayed* text, unlike a + # naive round() here which would bake rounding error + # into the actual driven value once the callback + # reads it back from env.values. + min=float(lo_disp), + max=float(hi_disp), + step=step, + value=float(val_disp), + desc=f"{robot.name} joint {j}", + unit=unit, + precision=2, + ), + name=f"q{j}", + ) + + update_pose_labels(q) + self.step() + + if block: + # Unbounded (duration=None): keep responding to slider drags + # for as long as the browser stays connected, same as any + # other Swift script's own `while True: env.step(dt)` loop -- + # just with run()'s disconnect-awareness (and ^C handling) + # folded in instead of looping forever after the tab is gone. + # Returns normally on a disconnect (graceful or mid-step); + # only ^C skips the rest of this method (raises SystemExit). + self.run() + + # PyPlot's teach() mutates robot.q throughout its own session, + # so a caller naturally finds the final taught pose in + # robot.q once teach() returns -- see BaseRobot.teach()'s + # docstring. Swift's AssemblyHandle deliberately doesn't + # mirror handle.q into robot.q during the session (that's + # the whole point of the refactor -- see jhavl/swift#85), but + # write it back once, here, at the point the session ends, + # so callers see the same thing regardless of backend. A + # deliberate one-time exception for this specific + # single-owner interactive session, not a general precedent + # -- the same "stateless over stateful" tension PyPlot's own + # teach() already has (see desiderata.md), just accepted + # here rather than solved. + robot.q = handle.q.copy() + + return True + __all__ = [ "Swift", diff --git a/src/roboticstoolbox/robot/BaseRobot.py b/src/roboticstoolbox/robot/BaseRobot.py index e89167488..62c430c43 100644 --- a/src/roboticstoolbox/robot/BaseRobot.py +++ b/src/roboticstoolbox/robot/BaseRobot.py @@ -2138,15 +2138,15 @@ def teach( limits: ArrayLike | None = None, vellipse: bool = False, fellipse: bool = False, - backend: L["pyplot", "pyplot2"] | None = None, + backend: L["swift", "pyplot", "pyplot2"] | None = None, ) -> Connector: """ Graphical teach pendant - ``robot.teach(q)`` creates a matplotlib plot which allows the user to - "drive" a graphical robot using a graphical slider panel. The robot's - inital joint configuration is ``q``. The plot will autoscale with an - aspect ratio of 1. + ``robot.teach(q)`` opens a graphical view (PyPlot or Swift -- + see ``backend``) which allows the user to "drive" a graphical + robot using a graphical slider panel. The robot's inital joint + configuration is ``q``. ``robot.teach()`` as above except the robot's stored value of ``q`` is used. @@ -2155,20 +2155,33 @@ def teach( if not supplied will use the stored q values). :param block: Block operation of the code and keep the figure open :param limits: Custom view limits for the plot. If not supplied will - autoscale, [x1, x2, y1, y2, z1, z2] + autoscale, [x1, x2, y1, y2, z1, z2] (this option is for + 'pyplot'/'pyplot2' only) :param vellipse: (Plot Option) Plot the velocity ellipse at the end-effector (this option is for 'pyplot' only) :param fellipse: (Plot Option) Plot the force ellipse at the end-effector (this option is for 'pyplot' only) + :param backend: The graphical backend to use -- 'swift', 'pyplot', + or 'pyplot2'. Defaults to whatever :meth:`plot` would pick for + this robot (see its own ``backend`` for the resolution rule). - :returns: A reference to the PyPlot object which controls the matplotlib figure + :returns: A reference to the environment object which controls + the figure/view .. rubric:: Notes - Program execution is blocked until the teach window is dismissed. If ``block=False`` the method is non-blocking but - you need to poll the window manager to ensure that the window + you need to poll the window manager (PyPlot) or keep calling + ``env.step()`` yourself (Swift) to ensure the window/panel remains responsive. + - Once ``teach()`` returns (``block=True``), ``robot.q`` holds + the final pose the sliders were left at -- true for every + backend, even though Swift's own live joint state during the + session lives on the returned environment's handle + (``env.swift_objects[0].q``), not ``robot.q`` itself, to keep + the robot model plain and shareable while teaching is + in progress (see swift's ``AssemblyHandle``). - The slider limits are derived from the joint limit properties. If not set then: @@ -2192,8 +2205,8 @@ def teach( ) # Add the self to the figure in readonly mode - env.launch("Teach " + self.name, limits=limits) - env.add( + env.launch(name="Teach " + self.name, limits=limits) + handle = env.add( self, readonly=True, # jointaxes=jointaxes, @@ -2204,7 +2217,12 @@ def teach( ) self._active_plot_env = env - env._add_teach_panel(self, q) + # True if _add_teach_panel already fully handled block=True itself + # (Swift's own env.run() loop -- see its docstring) -- PyPlot/ + # PyPlot2 never do (their own env.hold() below is what actually + # enters matplotlib's blocking GUI mainloop), so their + # _add_teach_panel implicitly returns None here, same as always. + already_blocked = env._add_teach_panel(self, q, handle, block) if vellipse: vell = self.vellipse(q, centre="ee", scale=0.5, add=False) @@ -2214,8 +2232,13 @@ def teach( fell = self.fellipse(q, centre="ee", add=False) env.add(fell) - # Keep the plot open - if block: # pragma nocover + # Keep the plot open -- skipped if the backend already blocked + # itself above: calling this a second time isn't just wasteful, + # it can hang outright for Swift (env.hold()'s disconnect-poll + # never expires in headless mode, and even non-headless there's + # a race where close() doesn't synchronously guarantee the + # websocket's been dropped from socket.USERS before returning). + if block and not already_blocked: # pragma nocover env.hold() return env diff --git a/tests/test_BaseRobot.py b/tests/test_BaseRobot.py index 14277d1f3..09f059271 100644 --- a/tests/test_BaseRobot.py +++ b/tests/test_BaseRobot.py @@ -626,3 +626,77 @@ def test_teach(self): robot = rtb.models.ETS.Panda() e = robot.teach(q=None, block=False, vellipse=True, fellipse=True) e.close() + + def test_teach_swift(self): + try: + import swift # noqa: F401 + except ImportError: + self.skipTest("swift-sim not installed") + + import os + from unittest.mock import patch + + robot = rtb.models.Panda() + with patch.dict(os.environ, {"SWIFT_HEADLESS": "1"}): + e = robot.teach(q=robot.qr, block=False, backend="swift") + try: + self.assertEqual(type(e).__name__, "Swift") + e.step(0.05) + nt.assert_almost_equal(e.swift_objects[0].q, robot.qr) + finally: + e.close() + + def test_teach_default_backend_resolves_to_swift(self): + # rtb.models.Panda() has hasgeometry=True, so teach() with no + # explicit backend= should resolve to Swift now that + # supports_teach=True -- matches plot()'s existing default, but + # is a real behaviour change (used to only ever be PyPlot/PyPlot2, + # since Swift always raised TypeError before this). + try: + import swift # noqa: F401 + except ImportError: + self.skipTest("swift-sim not installed") + + import os + from unittest.mock import patch + + robot = rtb.models.Panda() + self.assertTrue(robot.hasgeometry) + with patch.dict(os.environ, {"SWIFT_HEADLESS": "1"}): + e = robot.teach(q=robot.qr, block=False) + try: + self.assertEqual(type(e).__name__, "Swift") + finally: + e.close() + + def test_teach_swift_writes_final_q_back_to_robot(self): + # PyPlot's teach() mutates robot.q throughout its own session, so + # a caller naturally finds the final taught pose in robot.q once + # teach() returns. Swift's AssemblyHandle deliberately never + # mirrors handle.q into robot.q *during* a session -- but + # _add_teach_panel() must still write it back once, at the point + # the session ends (block=True), so callers see the same thing + # regardless of backend. + try: + from roboticstoolbox.backends.swift import Swift + except ImportError: + self.skipTest("swift-sim not installed") + + import os + from unittest.mock import patch + + robot = rtb.models.Panda() + + # block=True's own step loop (env.run()) would run forever in + # headless mode (nothing to disconnect from) -- not what this + # test cares about, only that the write-back after it runs. + # Patching it to a no-op isolates that. + with patch.object(Swift, "run", lambda self: None): + with patch.dict(os.environ, {"SWIFT_HEADLESS": "1"}): + e = robot.teach(q=robot.qz, block=True, backend="swift") + try: + handle = e.swift_objects[0] + nt.assert_almost_equal(robot.q, handle.q) + nt.assert_almost_equal(robot.q, robot.qz) + finally: + e.close() diff --git a/tests/test_backend_capabilities.py b/tests/test_backend_capabilities.py index d381e692e..9bfb7d768 100644 --- a/tests/test_backend_capabilities.py +++ b/tests/test_backend_capabilities.py @@ -53,7 +53,7 @@ def test_pyplot_instance_supports_ellipse(self): class TestSwiftCapabilities(unittest.TestCase): - """RTB Swift wrapper opts out of teach and ellipse.""" + """RTB Swift wrapper supports teach but opts out of ellipse (for now).""" def setUp(self): # Skip if swift-sim is not installed @@ -62,18 +62,18 @@ def setUp(self): except (ImportError, ModuleNotFoundError): self.skipTest("swift-sim not installed") - def test_swift_class_supports_teach_false(self): + def test_swift_class_supports_teach_true(self): from roboticstoolbox.backends.swift import Swift - self.assertFalse(Swift.supports_teach) + self.assertTrue(Swift.supports_teach) def test_swift_class_supports_ellipse_false(self): from roboticstoolbox.backends.swift import Swift self.assertFalse(Swift.supports_ellipse) - def test_load_backend_swift_supports_teach_false(self): + def test_load_backend_swift_supports_teach_true(self): from roboticstoolbox.backends import load_backend env = load_backend("swift") - self.assertFalse(env.supports_teach) + self.assertTrue(env.supports_teach) def test_load_backend_swift_supports_ellipse_false(self): from roboticstoolbox.backends import load_backend From 17bcfadd6cd9c806486a3e8c79a03cfae661208d Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Sat, 22 Aug 2026 11:34:31 +1000 Subject: [PATCH 2/2] fix(swift): use label kwarg/property, not deprecated desc Follow-up to this same PR's teach panel: swift-sim renamed desc to label on all SwiftElement subclasses (jhavl/swift#135) while this was in flight -- desc still works via a deprecation shim, but update to the new name rather than carry the warning forward. --- examples/teach_swift.py | 14 +++++++------- src/roboticstoolbox/backends/swift/__init__.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/teach_swift.py b/examples/teach_swift.py index 4fdb47c71..50163a59f 100644 --- a/examples/teach_swift.py +++ b/examples/teach_swift.py @@ -41,12 +41,12 @@ def update_pose_labels(q): T = robot.fkine(q) t = np.round(T.t, 3) r = np.round(T.rpy(unit="deg"), 3) - pose_labels[0].desc = f"x: {t[0]}" - pose_labels[1].desc = f"y: {t[1]}" - pose_labels[2].desc = f"z: {t[2]}" - pose_labels[3].desc = f"r: {r[0]}°" - pose_labels[4].desc = f"p: {r[1]}°" - pose_labels[5].desc = f"y: {r[2]}°" + pose_labels[0].label = f"x: {t[0]}" + pose_labels[1].label = f"y: {t[1]}" + pose_labels[2].label = f"z: {t[2]}" + pose_labels[3].label = f"r: {r[0]}°" + pose_labels[4].label = f"p: {r[1]}°" + pose_labels[5].label = f"y: {r[2]}°" def teach_update(t, values): @@ -84,7 +84,7 @@ def teach_update(t, values): max=float(hi_disp), step=step, value=float(val_disp), - desc=f"{robot.name} joint {j}", + label=f"{robot.name} joint {j}", unit=unit, precision=2, ), diff --git a/src/roboticstoolbox/backends/swift/__init__.py b/src/roboticstoolbox/backends/swift/__init__.py index 817134ee9..c45513393 100644 --- a/src/roboticstoolbox/backends/swift/__init__.py +++ b/src/roboticstoolbox/backends/swift/__init__.py @@ -68,12 +68,12 @@ def update_pose_labels(qv): T = robot.fkine(qv) t = np.round(T.t, 3) r = np.round(T.rpy(unit="deg"), 3) - pose_labels[0].desc = f"x: {t[0]}" - pose_labels[1].desc = f"y: {t[1]}" - pose_labels[2].desc = f"z: {t[2]}" - pose_labels[3].desc = f"r: {r[0]}°" - pose_labels[4].desc = f"p: {r[1]}°" - pose_labels[5].desc = f"y: {r[2]}°" + pose_labels[0].label = f"x: {t[0]}" + pose_labels[1].label = f"y: {t[1]}" + pose_labels[2].label = f"z: {t[2]}" + pose_labels[3].label = f"r: {r[0]}°" + pose_labels[4].label = f"p: {r[1]}°" + pose_labels[5].label = f"y: {r[2]}°" def teach_update(t, values): # Sliders display revolute joints in degrees, prismatic in @@ -112,7 +112,7 @@ def teach_update(t, values): max=float(hi_disp), step=step, value=float(val_disp), - desc=f"{robot.name} joint {j}", + label=f"{robot.name} joint {j}", unit=unit, precision=2, ),