Skip to content

Commit a982e98

Browse files
authored
Command Validation and Property Inheritance (#23)
Refactor robot controllers for unit standardization, shared validation, and extensibility - Standardize units across all controllers (`Dobot`, `ElephantRobotics`, `Fanuc`, `Jaka`) using shared `units` (`uu`) and `CommandCheck` (`cc`) modules. - Convert all internal joint values to radians, with protocol-specific conversions in `move_joints` and `move_cartesian`. - Centralize motion command validation in `CommandCheck`, removing duplicated per-class logic. - Update `Jaka` methods to use shared utilities for conversion and validation. - Migrate all controllers to inherit from `Properties` for consistent attribute access and easier extension. - Expand robot registry to include `UR3`, `UR10`, and `UR16`; comment out `OnRobot`. - Remove deprecated `armctl/angle_utils.py` in favor of `units`. - Update documentation and diagrams in `README.md` and formatting guidance in `CONTRIBUTING.md`. - Apply minor fixes: logging initialization and `Pro600` home position handling.
1 parent 29787cf commit a982e98

27 files changed

Lines changed: 802 additions & 489 deletions

CONTRIBUTING.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,7 @@ poetry install --with dev
146146
To ensure consistency and code quality, run the following commands before submitting your changes:
147147

148148
```bash
149-
poetry run black . && \
150-
poetry run isort . && \
151-
poetry run ruff format . && \
152-
poetry run mypy armctl
149+
poetry run ruff format .
153150
```
154151

155152
> All code must be properly formatted and pass type checks. Please resolve any issues reported by these tools prior to opening a pull request.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ The properties template exposes key robot class attributes as variables, allowin
230230
Below is a high-level diagram illustrating the architecture of the `armctl` library. This design emphasizes the careful templatization of connection and control APIs, ensuring a consistent and extensible interface across different manufacturers and robot series.
231231

232232
<p align="center">
233-
<img src="https://raw.githubusercontent.com/MGross21/armctl/main/assets/images/template_overview_mermaid.png" alt="Template Overview" width="800">
233+
<img src="https://raw.githubusercontent.com/MGross21/armctl/main/assets/diagrams/structure.svg" alt="Template Overview" width="800">
234234
</p>
235235

236236
### System Logging

armctl/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,19 @@
2626
"ElephantRobotics",
2727
"Pro600",
2828
"UniversalRobots",
29+
"UR3",
2930
"UR5",
3031
"UR5e",
31-
"OnRobot",
32+
"UR10",
33+
"UR16",
34+
# "OnRobot",
3235
"Vention",
3336
"Jaka",
3437
]
3538

3639
__version__ = "0.3.2"
3740

41+
3842
class Logger:
3943
"""Global logger utility for armctl."""
4044

@@ -54,6 +58,7 @@ def enable():
5458
# Re-enable logging to its previous state
5559
logging.disable(logging.NOTSET)
5660

61+
5762
import os
5863

5964
if os.environ.get("ARMCTL_LOG", "").lower() in {"0", "false", "disable"}:

armctl/_blank/robot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import math
2020
from armctl.templates import Commands
2121
from armctl.templates import Properties
22-
from armctl.utils import Angle as au
22+
from armctl.utils import units as uu
2323
from armctl.utils import CommandCheck as cc
2424

2525
# Choose one of the following Communication Methods

armctl/angle_utils.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

armctl/dobot/dobot.py

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,75 @@
11
from armctl.templates import SerialController as SCT
2+
from armctl.templates import Commands
3+
from armctl.templates import Properties
4+
from armctl.utils import CommandCheck as cc
5+
from armctl.utils import units as uu
6+
import math
27

8+
### Notes ###
9+
# - Command Format: CMD(arg)
10+
# - Command units are degrees & mm.
311

4-
class Dobot(SCT):
12+
13+
class Dobot(SCT, Commands, Properties):
514
def __init__(self, ip: str, port: int):
615
super().__init__(ip, port)
7-
self.JOINT_RANGES = [
8-
(-135.00, 135.00),
9-
(-5.00, 80.00),
10-
(-10.00, 85.00),
11-
(-145.00, 145.00),
12-
]
13-
self.DOF = len(self.JOINT_RANGES)
16+
self.JOINT_RANGES = uu.joints2rad(
17+
[
18+
(-135.00, 135.00),
19+
(-5.00, 80.00),
20+
(-10.00, 85.00),
21+
(-145.00, 145.00),
22+
]
23+
)
24+
self.MAX_JOINT_VELOCITY = None
25+
self.MAX_JOINT_ACCELERATION = None
26+
1427
raise NotImplementedError(
1528
f"{self.__class__.__name__.upper()} is not yet supported."
1629
)
1730

1831
def sleep(self, seconds):
32+
cc.sleep(seconds)
1933
self.send_command(f"sleep({seconds})")
2034

21-
def move_joints(self, pos, *args, **kwargs) -> str:
35+
def move_joints(self, pos) -> str:
2236
"MovJ"
2337

24-
if len(pos) != kwargs.get("DOF", 4):
25-
raise ValueError("Joint positions must have 4 elements")
26-
27-
for j, (lower, upper) in enumerate(self.JOINT_RANGES):
28-
if not (lower <= pos[j] <= upper):
29-
raise ValueError(
30-
f"Joint {j + 1} angle out of range: {lower} ~ {upper}"
31-
)
38+
cc.move_joints(self, pos)
3239

3340
command = "MOVJ({})".format(",".join(map(str, pos)))
3441
return self.send_command(command)
3542

3643
def move_cartesian(self, pose) -> str:
37-
"MOVEL"
38-
39-
if len(pose) == 3:
40-
pose.append(0)
41-
42-
# Now check again if the robot pose has 4 elements
43-
if len(pose) != 4:
44-
raise ValueError(
45-
"Robot pose must have 3 ([x, y, z]) or 4 elements: [x, y, z, rz]"
44+
"""
45+
Moves the robot arm to a specified Cartesian position.
46+
47+
Parameters:
48+
pose (list or tuple): Target position as [x, y, z, r]. x, y, z are in meters and r is in radians.
49+
50+
Returns:
51+
str: The response from the robot after executing the MOVEL command.
52+
53+
Notes:
54+
- The method sends a MOVEL command to the robot controller.
55+
- The pose is expected in m for x, y, z and radians for r.
56+
"""
57+
cc.move_cartesian(self, pose)
58+
# Convert x, y, z from meters to millimeters, r from radians to degrees
59+
60+
command = "MOVEL({})".format(
61+
",".join(
62+
map(
63+
str,
64+
[
65+
pose[0] * 1000, # x in mm
66+
pose[1] * 1000, # y in mm
67+
pose[2] * 1000, # z in mm
68+
math.degrees(pose[3]), # r in degrees
69+
],
70+
)
4671
)
47-
48-
command = "MOVEL({})".format(",".join(map(str, pose)))
72+
)
4973
return self.send_command(command)
5074

5175
def get_joint_positions(self):
@@ -59,17 +83,3 @@ def stop_motion(self):
5983

6084
def get_robot_state(self):
6185
pass
62-
63-
def move_arc(self, command):
64-
"""The trajectory of ARC mode is an arc, which is determined by three points (the current point, any point and the end point on the arc)"""
65-
if len(command) != 3:
66-
raise ValueError("Invalid ARC command. Must have 3 points")
67-
68-
self.send_command(f"ARC({','.join(map(str, command))})")
69-
70-
def move_jump(self, command):
71-
"""If the movement of two points is required to lift upwards by amount of height, such as sucking up, grabbing, you can choose JUMP"""
72-
if len(command) != 2:
73-
raise ValueError("Invalid JUMP command. Must have 2 points")
74-
75-
self.send_command(f"JUMP({','.join(map(str, command))})")

armctl/elephant_robotics/elephant_robotics.py

Lines changed: 41 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
import time
22

33
from armctl.templates import Commands
4+
from armctl.templates import Properties
45
from armctl.templates import SocketController as SCT
56

7+
from armctl.utils import CommandCheck as cc
8+
from armctl.utils import units as uu
69

7-
class ElephantRobotics(SCT, Commands):
10+
## Notes
11+
# - Command Format: CMD(arg)
12+
# - Command units are degrees & mm.
13+
14+
15+
class ElephantRobotics(SCT, Commands, Properties):
816
def __init__(self, ip: str, port: int):
917
super().__init__(ip, port)
10-
self.JOINT_RANGES = [
11-
(-180.00, 180.00),
12-
(-270.00, 90.00),
13-
(-150.00, 150.00),
14-
(-260.00, 80.00),
15-
(-168.00, 168.00),
16-
(-174.00, 174.00),
17-
]
18-
self.DOF = len(self.JOINT_RANGES)
18+
self.JOINT_RANGES = uu.joints2rad(
19+
[
20+
(-180.00, 180.00),
21+
(-270.00, 90.00),
22+
(-150.00, 150.00),
23+
(-260.00, 80.00),
24+
(-168.00, 168.00),
25+
(-174.00, 174.00),
26+
]
27+
)
28+
self.MAX_JOINT_VELOCITY = uu.deg2rad(2000)
29+
self.MAX_JOINT_ACCELERATION = None
1930

2031
def connect(self):
2132
super().connect() # Socket Connection
@@ -43,46 +54,35 @@ def _waitforfinish(self):
4354
time.sleep(0.25)
4455

4556
def sleep(self, seconds):
46-
assert isinstance(
47-
seconds, (int, float)
48-
), "Seconds must be a numeric value."
49-
assert seconds >= 0, "Seconds must be a non-negative value."
57+
cc.sleep(seconds)
5058
self.send_command(f"wait({seconds})")
5159
time.sleep(seconds)
5260

53-
def move_joints(self, pos: list[float], speed: int = 500) -> None:
61+
def move_joints(
62+
self, pos: list[float], speed: int = uu.deg2rad(500)
63+
) -> None:
5464
"""
5565
Move the robot to the specified joint positions.
5666
5767
Parameters
5868
----------
5969
pos : list of float
60-
Joint positions in degrees [j1, j2, j3, j4, j5, j6].
70+
Joint positions in radians [j1, j2, j3, j4, j5, j6].
6171
speed : int, optional
62-
Speed of the movement, range 0 ~ 2000 (default: 200).
63-
DOF : int, optional
64-
Degrees of freedom (default: 6).
72+
Speed of the movement, range `0` ~ `math.radians(2000)` (default: `math.radians(500)`).
6573
"""
6674

67-
if len(pos) != self.DOF:
68-
raise ValueError("Joint positions must have 6 elements")
75+
cc.move_joints(self, pos, speed)
6976

70-
for i, (low, high) in enumerate(self.JOINT_RANGES):
71-
if not (low <= pos[i] <= high):
72-
raise ValueError(
73-
f"Joint {i + 1} angle out of range: {low} ~ {high}"
74-
)
77+
pos_deg = uu.joints2deg(pos)
78+
speed_deg = uu.rad2deg(speed)
7579

76-
if not (0 <= speed <= 2000):
77-
raise ValueError("Speed out of range: 0 ~ 2000")
80+
command = f"set_angles({','.join(map(str, pos_deg))},{speed_deg})"
81+
response = self.send_command(command)
7882

79-
command = "set_angles"
80-
response = self.send_command(
81-
f"{command}({','.join(map(str, pos))},{speed})"
83+
assert response == f"{command}:[ok]", (
84+
f"Failed to move joints: {response}"
8285
)
83-
assert (
84-
response == f"{command}:[ok]"
85-
), f"Failed to move joints: {response}"
8686

8787
while any(
8888
abs(a - b) > 3 for a, b in zip(self.get_joint_positions(), pos)
@@ -92,7 +92,7 @@ def move_joints(self, pos: list[float], speed: int = 500) -> None:
9292
def move_cartesian(
9393
self,
9494
pose: tuple[float, float, float, float, float, float],
95-
speed: int = 500,
95+
speed: int = uu.deg2rad(500),
9696
) -> None:
9797
"""
9898
Move the robot to the specified Cartesian coordinates.
@@ -102,17 +102,15 @@ def move_cartesian(
102102
pose : tuple of float
103103
Cartesian coordinates in the format `[x, y, z, rx, ry, rz]`.
104104
speed : int, optional
105-
Speed of the movement, range 0 ~ 2000 (default: 500).
105+
Speed of the movement, range `0` ~ `math.radians(2000)` (default: `math.radians(500)`).
106106
"""
107107

108-
if not (0 <= speed <= 2000):
109-
raise ValueError("Speed out of range: 0 ~ 2000")
110-
if len(pose) != 6:
111-
raise ValueError(
112-
"Robot pose must have 6 elements: [x, y, z, rx, ry, rz]"
113-
)
108+
cc.move_cartesian(self, pose)
109+
110+
pose_deg = uu.pose2deg(pose)
111+
speed_deg = uu.rad2deg(speed)
114112

115-
command = f"set_coords({','.join(map(str, pose))},{speed})"
113+
command = f"set_coords({','.join(map(str, pose_deg))},{speed_deg})"
116114

117115
assert self.send_command(command) == "set_coords:[ok]"
118116

armctl/elephant_robotics/mycobot.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
1-
from .elephant_robotics import ElephantRobotics
1+
from .elephant_robotics import ElephantRobotics, uu
2+
import math
23

34

45
class Pro600(ElephantRobotics):
56
def __init__(self, ip: str = "192.168.1.159", port: int = 5001):
67
"""Elephant Robotics myCobot Pro600"""
78
super().__init__(ip, port)
8-
self.HOME_POSITION = [0, -90, 90, -90, -90, 0]
9-
self.JOINT_RANGES = [
10-
(-180.00, 180.00),
11-
(-270.00, 90.00),
12-
(-150.00, 150.00),
13-
(-260.00, 80.00),
14-
(-168.00, 168.00),
15-
(-174.00, 174.00),
16-
]
17-
self.DOF = len(self.JOINT_RANGES)
18-
19-
self.__class__.__name__ = f"{self.__class__.__bases__[0].__name__} {__name__.split('.')[-1]} {self.__class__.__name__}"
9+
self.HOME_POSITION = uu.joints2rad([0, -90, 90, -90, -90, 0])
2010

2111
def home(self):
2212
"""

armctl/fanuc/fanuc.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
from armctl.templates import Commands
2+
from armctl.templates import Properties
23
from armctl.templates import PLCController as PLC
34

5+
from armctl.utils import CommandCheck as cc
6+
47

58
# Non-Operational (1/31/2025)
6-
class Fanuc(PLC, Commands):
9+
class Fanuc(PLC, Commands, Properties):
710
def __init__(self, ip: str, port: int):
811
super().__init__(ip, port)
12+
self.JOINT_RANGES = None
13+
self.MAX_JOINT_VELOCITY = None
14+
self.MAX_JOINT_ACCELERATION = None
915
raise NotImplementedError(
1016
f"{self.__class__.__name__.upper()} is not yet supported."
1117
)
1218

1319
def move_joints(self, pos, speed=1.0):
20+
cc.move_joints(self, pos, speed)
1421
return self.send_command(
1522
{"type": "move_joints", "positions": pos, "speed": speed}
1623
)
1724

1825
def move_cartesian(self, pose, speed=1.0):
26+
cc.move_cartesian(self, pose)
1927
return self.send_command(
2028
{"type": "move_cartesian", "position": pose, "speed": speed}
2129
)

0 commit comments

Comments
 (0)